mirror of
https://github.com/nkanaev/yarr.git
synced 2025-11-07 09:59:38 +00:00
Compare commits
99 Commits
v2.4
...
29d9ec6ef1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29d9ec6ef1 | ||
|
|
d2224399e2 | ||
|
|
67fbed7f6b | ||
|
|
c1df3f8068 | ||
|
|
0aed9b51a9 | ||
|
|
0bd7a66086 | ||
|
|
2b6823a277 | ||
|
|
dd7ed84a6c | ||
|
|
2c6a5ca971 | ||
|
|
5bf7647cba | ||
|
|
f721034ae5 | ||
|
|
a32361fab2 | ||
|
|
572e489db6 | ||
|
|
efcb6f8bf0 | ||
|
|
7e367ef537 | ||
|
|
b9a3326a98 | ||
|
|
484b155a3c | ||
|
|
9cba4e8deb | ||
|
|
749d7b682e | ||
|
|
35850d6310 | ||
|
|
15db17d834 | ||
|
|
a0d86e884a | ||
|
|
acf97c8a3b | ||
|
|
34bf9e5160 | ||
|
|
4420f3a8ae | ||
|
|
8d2ea6cf8a | ||
|
|
e244237474 | ||
|
|
ff81c9d689 | ||
|
|
11d99f106e | ||
|
|
b8afa82a81 | ||
|
|
097a2da5cb | ||
|
|
e6d32946c1 | ||
|
|
fe4eaa4b8d | ||
|
|
48a671b285 | ||
|
|
011c9c7668 | ||
|
|
f06fc1f750 | ||
|
|
0e88d4284d | ||
|
|
1615c6869f | ||
|
|
800f43b299 | ||
|
|
15bff0a0c4 | ||
|
|
e1481f4aac | ||
|
|
7ef97ee6db | ||
|
|
d785fe4c5a | ||
|
|
5254df53dc | ||
|
|
7301eab99c | ||
|
|
ad138c3017 | ||
|
|
b09c95d7ea | ||
|
|
64611a0dd3 | ||
|
|
321ad7608f | ||
|
|
2a8b6ea935 | ||
|
|
e9cbea500b | ||
|
|
223039b2c6 | ||
|
|
7402dfc4e6 | ||
|
|
6b12715506 | ||
|
|
2dc58c5c8e | ||
|
|
0cef51c6ac | ||
|
|
2a4d974965 | ||
|
|
f71792d6a5 | ||
|
|
b571042c5d | ||
|
|
349c966c63 | ||
|
|
4a42b239cc | ||
|
|
b9b3d2350c | ||
|
|
b13cd85f0b | ||
|
|
daffd721eb | ||
|
|
24232d72e9 | ||
|
|
4983e18e23 | ||
|
|
e1954e4cba | ||
|
|
58420ae52b | ||
|
|
b01f71de1a | ||
|
|
379aaed39e | ||
|
|
dc20932060 | ||
|
|
96835ebd33 | ||
|
|
c896f779b5 | ||
|
|
5f606b1c40 | ||
|
|
9d5b8d99f7 | ||
|
|
13c047fc21 | ||
|
|
55751b3eb6 | ||
|
|
b961502a17 | ||
|
|
a895145586 | ||
|
|
5aec3b4dab | ||
|
|
d787060a24 | ||
|
|
c1a29418eb | ||
|
|
17847f999c | ||
|
|
3adcddc70c | ||
|
|
c76ff26bd6 | ||
|
|
50f8648f64 | ||
|
|
5f82a9e339 | ||
|
|
3278ba4eac | ||
|
|
9fc72f8b68 | ||
|
|
b7b707bd43 | ||
|
|
7cf27e0fde | ||
|
|
66f2a973a3 | ||
|
|
7ecbbff18a | ||
|
|
850ce195a0 | ||
|
|
479aebd023 | ||
|
|
9b178d1fb3 | ||
|
|
3ab098db5c | ||
|
|
6d16e93008 | ||
|
|
98934daee4 |
25
.github/actions/prepare/action.yml
vendored
Normal file
25
.github/actions/prepare/action.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Build & Upload
|
||||||
|
inputs:
|
||||||
|
id:
|
||||||
|
description: artifact name
|
||||||
|
required: true
|
||||||
|
cmd:
|
||||||
|
description: command to run
|
||||||
|
required: true
|
||||||
|
out:
|
||||||
|
description: path to output file
|
||||||
|
required: true
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: compile
|
||||||
|
run: ${{ inputs.cmd }}
|
||||||
|
shell: bash
|
||||||
|
- name: archive
|
||||||
|
run: tar -cvf ${{ inputs.out }}.tar ${{ inputs.out }}
|
||||||
|
shell: bash
|
||||||
|
- name: upload
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.id }}
|
||||||
|
path: ${{ inputs.out }}.tar
|
||||||
38
.github/workflows/build-docker-image
vendored
Normal file
38
.github/workflows/build-docker-image
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Publish Docker Image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: [ 'v*.*.*', 'v*.*', 'v*', 'latest' ]
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: nkanaev/yarr
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./etc/dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
203
.github/workflows/build.yml
vendored
203
.github/workflows/build.yml
vendored
@@ -1,144 +1,143 @@
|
|||||||
name: build
|
name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ['v*', 'test*']
|
tags:
|
||||||
|
- v*
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_macos:
|
build_macos:
|
||||||
name: Build for MacOS
|
name: Build for MacOS
|
||||||
runs-on: macos-13
|
runs-on: macos-13
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout"
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
go-version: '^1.18'
|
||||||
- name: "Setup Go"
|
- name: Build arm64 gui
|
||||||
uses: actions/setup-go@v2
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
go-version: '^1.17'
|
id: darwin_arm64_gui
|
||||||
- name: Cache Go Modules
|
cmd: make darwin_arm64_gui
|
||||||
uses: actions/cache@v2
|
out: out/darwin_arm64_gui/yarr.app
|
||||||
|
- name: Build amd64 gui
|
||||||
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
id: darwin_amd64_gui
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
cmd: make darwin_amd64_gui
|
||||||
restore-keys: |
|
out: out/darwin_amd64_gui/yarr.app
|
||||||
${{ runner.os }}-go-
|
- name: Build arm64 cli
|
||||||
- name: "Build"
|
uses: ./.github/actions/prepare
|
||||||
run: make build_macos
|
|
||||||
- name: Upload
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
with:
|
||||||
name: macos
|
id: darwin_arm64
|
||||||
path: _output/macos/yarr.app
|
cmd: make darwin_arm64
|
||||||
|
out: out/darwin_arm64/yarr
|
||||||
|
- name: Build amd64 cli
|
||||||
|
uses: ./.github/actions/prepare
|
||||||
|
with:
|
||||||
|
id: darwin_amd64
|
||||||
|
cmd: make darwin_amd64
|
||||||
|
out: out/darwin_amd64/yarr
|
||||||
|
|
||||||
build_windows:
|
build_windows:
|
||||||
name: Build for Windows
|
name: Build for Windows
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout"
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
go-version: '^1.18'
|
||||||
- name: "Setup Go"
|
- name: Build amd64 gui
|
||||||
uses: actions/setup-go@v2
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
go-version: '^1.17'
|
id: windows_amd64_gui
|
||||||
- name: Cache Go Modules
|
cmd: make windows_amd64_gui
|
||||||
uses: actions/cache@v2
|
out: out/windows_amd64_gui/yarr.exe
|
||||||
|
- name: Build arm64 gui
|
||||||
|
if: false
|
||||||
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
id: windows_arm64_gui
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
cmd: make windows_arm64_gui
|
||||||
restore-keys: |
|
out: out/windows_arm64_gui/yarr.exe
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: "Build"
|
|
||||||
run: make build_windows
|
|
||||||
- name: Upload
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: windows
|
|
||||||
path: _output/windows/yarr.exe
|
|
||||||
|
|
||||||
build_linux:
|
build_multi_cli:
|
||||||
name: Build for Linux
|
name: Build for Windows/MacOS/Linux CLI
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout"
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
go-version: '^1.18'
|
||||||
- name: "Setup Go"
|
- name: Setup Zig
|
||||||
uses: actions/setup-go@v2
|
uses: mlugg/setup-zig@v1
|
||||||
with:
|
with:
|
||||||
go-version: '^1.17'
|
version: 0.14.0
|
||||||
- name: Cache Go Modules
|
- name: Build linux/amd64
|
||||||
uses: actions/cache@v2
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
id: linux_amd64
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
cmd: make linux_amd64
|
||||||
restore-keys: |
|
out: out/linux_amd64/yarr
|
||||||
${{ runner.os }}-go-
|
- name: Build linux/arm64
|
||||||
- name: "Build"
|
uses: ./.github/actions/prepare
|
||||||
run: make build_linux
|
|
||||||
- name: Upload
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
with:
|
||||||
name: linux
|
id: linux_arm64
|
||||||
path: _output/linux/yarr
|
cmd: make linux_arm64
|
||||||
|
out: out/linux_arm64/yarr
|
||||||
|
- name: Build linux/armv7
|
||||||
|
uses: ./.github/actions/prepare
|
||||||
|
with:
|
||||||
|
id: linux_armv7
|
||||||
|
cmd: make linux_armv7
|
||||||
|
out: out/linux_armv7/yarr
|
||||||
|
- name: Build windows/amd64
|
||||||
|
uses: ./.github/actions/prepare
|
||||||
|
with:
|
||||||
|
id: windows_amd64
|
||||||
|
cmd: make windows_amd64
|
||||||
|
out: out/windows_amd64/yarr
|
||||||
|
- name: Build windows/arm64
|
||||||
|
uses: ./.github/actions/prepare
|
||||||
|
with:
|
||||||
|
id: windows_arm64
|
||||||
|
cmd: make windows_arm64
|
||||||
|
out: out/windows_arm64/yarr
|
||||||
|
|
||||||
create_release:
|
create_release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ !contains(github.ref, 'test') }}
|
needs: [build_macos, build_windows, build_multi_cli]
|
||||||
needs: [build_macos, build_windows, build_linux]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Create Release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
id: create_release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: ${{ github.ref }}
|
|
||||||
draft: true
|
|
||||||
prerelease: true
|
|
||||||
- name: Download Artifacts
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4.1.7
|
||||||
with:
|
with:
|
||||||
path: .
|
path: .
|
||||||
- name: Preparation
|
- name: Preparation
|
||||||
run: |
|
run: |
|
||||||
|
set -ex
|
||||||
ls -R
|
ls -R
|
||||||
chmod u+x macos/Contents/MacOS/yarr
|
for tarfile in `ls **/*.tar`; do
|
||||||
chmod u+x linux/yarr
|
tar -xvf $tarfile
|
||||||
|
done
|
||||||
mv macos yarr.app && zip -r yarr-macos.zip yarr.app
|
for dir in out/*; do
|
||||||
mv windows/yarr.exe . && zip yarr-windows.zip yarr.exe
|
echo "Compressing: $dir"
|
||||||
mv linux/yarr . && zip yarr-linux.zip yarr
|
(test -d "$dir" && cd $dir && zip -r ../yarr_`basename $dir`.zip *)
|
||||||
- name: Upload MacOS
|
done
|
||||||
uses: actions/upload-release-asset@v1
|
ls out
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
draft: true
|
||||||
asset_path: ./yarr-macos.zip
|
prerelease: true
|
||||||
asset_name: yarr-${{ github.ref }}-macos64.zip
|
files: |
|
||||||
asset_content_type: application/zip
|
out/*.zip
|
||||||
- name: Upload Windows
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./yarr-windows.zip
|
|
||||||
asset_name: yarr-${{ github.ref }}-windows64.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
- name: Upload Linux
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./yarr-linux.zip
|
|
||||||
asset_name: yarr-${{ github.ref }}-linux64.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|||||||
19
.github/workflows/test.yml
vendored
Normal file
19
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '^1.18'
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: make test
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,8 @@
|
|||||||
/_output
|
/_output
|
||||||
|
/out
|
||||||
/yarr
|
/yarr
|
||||||
*.db
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
*.syso
|
*.syso
|
||||||
versioninfo.rc
|
versioninfo.rc
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var rsrc = `1 VERSIONINFO
|
|
||||||
FILEVERSION {VERSION_COMMA},0,0
|
|
||||||
PRODUCTVERSION {VERSION_COMMA},0,0
|
|
||||||
BEGIN
|
|
||||||
BLOCK "StringFileInfo"
|
|
||||||
BEGIN
|
|
||||||
BLOCK "080904E4"
|
|
||||||
BEGIN
|
|
||||||
VALUE "CompanyName", "Old MacDonald's Farm"
|
|
||||||
VALUE "FileDescription", "Yet another RSS reader"
|
|
||||||
VALUE "FileVersion", "{VERSION}"
|
|
||||||
VALUE "InternalName", "yarr"
|
|
||||||
VALUE "LegalCopyright", "nkanaev"
|
|
||||||
VALUE "OriginalFilename", "yarr.exe"
|
|
||||||
VALUE "ProductName", "yarr"
|
|
||||||
VALUE "ProductVersion", "{VERSION}"
|
|
||||||
END
|
|
||||||
END
|
|
||||||
BLOCK "VarFileInfo"
|
|
||||||
BEGIN
|
|
||||||
VALUE "Translation", 0x809, 1252
|
|
||||||
END
|
|
||||||
END
|
|
||||||
|
|
||||||
1 ICON "icon.ico"
|
|
||||||
`
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var version, outfile string
|
|
||||||
flag.StringVar(&version, "version", "0.0", "")
|
|
||||||
flag.StringVar(&outfile, "outfile", "versioninfo.rc", "")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
version_comma := strings.ReplaceAll(version, ".", ",")
|
|
||||||
|
|
||||||
out := strings.ReplaceAll(rsrc, "{VERSION}", version)
|
|
||||||
out = strings.ReplaceAll(out, "{VERSION_COMMA}", version_comma)
|
|
||||||
|
|
||||||
ioutil.WriteFile(outfile, []byte(out), 0644)
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>yarr</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>yarr</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>nkanaev.yarr</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>VERSION</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>yarr</string>
|
|
||||||
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string>icon</string>
|
|
||||||
<key>LSApplicationCategoryType</key>
|
|
||||||
<string>public.app-category.news</string>
|
|
||||||
|
|
||||||
<key>NSHighResolutionCapable</key>
|
|
||||||
<string>True</string>
|
|
||||||
|
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>10.13</string>
|
|
||||||
<key>LSUIElement</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
|
||||||
<string>Copyright © 2020 nkanaev. All rights reserved.</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
`
|
|
||||||
|
|
||||||
func run(cmd ...string) {
|
|
||||||
fmt.Println(cmd)
|
|
||||||
err := exec.Command(cmd[0], cmd[1:]...).Run()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var version, outdir string
|
|
||||||
flag.StringVar(&version, "version", "0.0", "")
|
|
||||||
flag.StringVar(&outdir, "outdir", "", "")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
outfile := "yarr"
|
|
||||||
|
|
||||||
binDir := path.Join(outdir, "yarr.app", "Contents/MacOS")
|
|
||||||
resDir := path.Join(outdir, "yarr.app", "Contents/Resources")
|
|
||||||
|
|
||||||
plistFile := path.Join(outdir, "yarr.app", "Contents/Info.plist")
|
|
||||||
pkginfoFile := path.Join(outdir, "yarr.app", "Contents/PkgInfo")
|
|
||||||
|
|
||||||
os.MkdirAll(binDir, 0700)
|
|
||||||
os.MkdirAll(resDir, 0700)
|
|
||||||
|
|
||||||
f, _ := ioutil.ReadFile(path.Join(outdir, outfile))
|
|
||||||
ioutil.WriteFile(path.Join(binDir, outfile), f, 0755)
|
|
||||||
|
|
||||||
ioutil.WriteFile(plistFile, []byte(strings.Replace(plist, "VERSION", version, 1)), 0644)
|
|
||||||
ioutil.WriteFile(pkginfoFile, []byte("APPL????"), 0644)
|
|
||||||
|
|
||||||
iconFile := path.Join(outdir, "icon.png")
|
|
||||||
iconsetDir := path.Join(outdir, "icon.iconset")
|
|
||||||
os.Mkdir(iconsetDir, 0700)
|
|
||||||
|
|
||||||
for _, res := range []int{1024, 512, 256, 128, 64, 32, 16} {
|
|
||||||
outfile := fmt.Sprintf("icon_%dx%d.png", res, res)
|
|
||||||
if res == 1024 || res == 64 {
|
|
||||||
outfile = fmt.Sprintf("icon_%dx%d@2x.png", res/2, res/2)
|
|
||||||
}
|
|
||||||
cmd := []string{
|
|
||||||
"sips", "-s", "format", "png", "--resampleWidth", strconv.Itoa(res),
|
|
||||||
iconFile, "--out", path.Join(iconsetDir, outfile),
|
|
||||||
}
|
|
||||||
run(cmd...)
|
|
||||||
}
|
|
||||||
|
|
||||||
icnsFile := path.Join(resDir, "icon.icns")
|
|
||||||
run("iconutil", "-c", "icns", iconsetDir, "-o", icnsFile)
|
|
||||||
}
|
|
||||||
44
build.md
44
build.md
@@ -1,44 +0,0 @@
|
|||||||
## Compilation
|
|
||||||
|
|
||||||
Install `Go >= 1.17` and `GCC`. Get the source code:
|
|
||||||
|
|
||||||
git clone https://github.com/nkanaev/yarr.git
|
|
||||||
|
|
||||||
Then run one of the corresponding commands:
|
|
||||||
|
|
||||||
# create an executable for the host os
|
|
||||||
make build_macos # -> _output/macos/yarr.app
|
|
||||||
make build_linux # -> _output/linux/yarr
|
|
||||||
make build_windows # -> _output/windows/yarr.exe
|
|
||||||
|
|
||||||
# host-specific cli version (no gui)
|
|
||||||
make build_default # -> _output/yarr
|
|
||||||
|
|
||||||
# ... or start a dev server locally
|
|
||||||
make serve # starts a server at http://localhost:7070
|
|
||||||
|
|
||||||
# ... or build a docker image
|
|
||||||
docker build -t yarr .
|
|
||||||
|
|
||||||
## ARM compilation
|
|
||||||
|
|
||||||
The instructions below are to cross-compile *yarr* to `Linux/ARM*`.
|
|
||||||
|
|
||||||
Build:
|
|
||||||
|
|
||||||
docker build -t yarr.arm -f dockerfile.arm .
|
|
||||||
|
|
||||||
Test:
|
|
||||||
|
|
||||||
# inside host
|
|
||||||
docker run -it --rm yarr.arm
|
|
||||||
|
|
||||||
# then, inside container
|
|
||||||
cd /root/out
|
|
||||||
qemu-aarch64 -L /usr/aarch64-linux-gnu/ yarr.arm64
|
|
||||||
|
|
||||||
Extract files from images:
|
|
||||||
|
|
||||||
CID=$(docker create yarr.arm)
|
|
||||||
docker cp -a "$CID:/root/out" .
|
|
||||||
docker rm "$CID"
|
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/nkanaev/yarr/src/platform"
|
"github.com/nkanaev/yarr/src/platform"
|
||||||
"github.com/nkanaev/yarr/src/server"
|
"github.com/nkanaev/yarr/src/server"
|
||||||
"github.com/nkanaev/yarr/src/storage"
|
"github.com/nkanaev/yarr/src/storage"
|
||||||
|
"github.com/nkanaev/yarr/src/worker"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version string = "0.0"
|
var Version string = "0.0"
|
||||||
@@ -89,12 +90,12 @@ func main() {
|
|||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
configPath, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Failed to get config dir: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if db == "" {
|
if db == "" {
|
||||||
|
configPath, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to get config dir: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
storagePath := filepath.Join(configPath, "yarr")
|
storagePath := filepath.Join(configPath, "yarr")
|
||||||
if err := os.MkdirAll(storagePath, 0755); err != nil {
|
if err := os.MkdirAll(storagePath, 0755); err != nil {
|
||||||
log.Fatal("Failed to create app config dir: ", err)
|
log.Fatal("Failed to create app config dir: ", err)
|
||||||
@@ -105,6 +106,7 @@ func main() {
|
|||||||
log.Printf("using db file %s", db)
|
log.Printf("using db file %s", db)
|
||||||
|
|
||||||
var username, password string
|
var username, password string
|
||||||
|
var err error
|
||||||
if authfile != "" {
|
if authfile != "" {
|
||||||
f, err := os.Open(authfile)
|
f, err := os.Open(authfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -131,6 +133,7 @@ func main() {
|
|||||||
log.Fatal("Failed to initialise database: ", err)
|
log.Fatal("Failed to initialise database: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
worker.SetVersion(Version)
|
||||||
srv := server.NewServer(store, addr)
|
srv := server.NewServer(store, addr)
|
||||||
|
|
||||||
if basepath != "" {
|
if basepath != "" {
|
||||||
56
doc/build.md
Normal file
56
doc/build.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
## Compilation
|
||||||
|
|
||||||
|
Prerequisies:
|
||||||
|
|
||||||
|
* Go >= 1.18
|
||||||
|
* C Compiler (GCC / Clang / ...)
|
||||||
|
* Zig >= 0.14.0 (optional, for cross-compiling CLI versions)
|
||||||
|
* binutils (optional, for building Windows GUI version)
|
||||||
|
|
||||||
|
Get the source code:
|
||||||
|
|
||||||
|
git clone https://github.com/nkanaev/yarr.git
|
||||||
|
|
||||||
|
Compile:
|
||||||
|
|
||||||
|
# create cli for the host OS/architecture
|
||||||
|
make host # out/yarr
|
||||||
|
|
||||||
|
# create GUI, works only in the target OS
|
||||||
|
make windows_amd64_gui # out/windows_amd64_gui/yarr.exe
|
||||||
|
make windows_arm64_gui # out/windows_arm64_gui/yarr.exe
|
||||||
|
make darwin_arm64_gui # out/darwin_arm64_gui/yarr.app
|
||||||
|
make darwin_amd64_gui # out/darwin_amd64_gui/yarr.app
|
||||||
|
|
||||||
|
# create cli, cross-compiles within any OS/architecture
|
||||||
|
make linux_amd64
|
||||||
|
make linux_arm64
|
||||||
|
make linux_armv7
|
||||||
|
make windows_amd64
|
||||||
|
make windows_arm64
|
||||||
|
|
||||||
|
# ... or build a docker image
|
||||||
|
docker build -t yarr -f etc/dockerfile .
|
||||||
|
|
||||||
|
## ARM compilation
|
||||||
|
|
||||||
|
The instructions below are to cross-compile *yarr* to `Linux/ARM*`.
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
docker build -t yarr.arm -f etc/dockerfile.arm .
|
||||||
|
|
||||||
|
Test:
|
||||||
|
|
||||||
|
# inside host
|
||||||
|
docker run -it --rm yarr.arm
|
||||||
|
|
||||||
|
# then, inside container
|
||||||
|
cd /root/out
|
||||||
|
qemu-aarch64 -L /usr/aarch64-linux-gnu/ yarr.arm64
|
||||||
|
|
||||||
|
Extract files from images:
|
||||||
|
|
||||||
|
CID=$(docker create yarr.arm)
|
||||||
|
docker cp -a "$CID:/root/out" .
|
||||||
|
docker rm "$CID"
|
||||||
@@ -1,4 +1,24 @@
|
|||||||
# upcoming
|
# upcoming
|
||||||
|
|
||||||
|
- (new) Fever API support (thanks to @icefed)
|
||||||
|
- (new) editable feed link (thanks to @adaszko)
|
||||||
|
- (new) switch to feed by clicking the title in the article page (thanks to @tarasglek for suggestion)
|
||||||
|
- (new) support multiple media links
|
||||||
|
- (fix) duplicate articles caused by the same feed addition (thanks to @adaszko)
|
||||||
|
- (fix) relative article links (thanks to @adazsko for the report)
|
||||||
|
- (fix) atom article links stored in id element (thanks to @adazsko for the report)
|
||||||
|
- (fix) parsing atom feed titles (thanks to @wnh)
|
||||||
|
- (fix) sorting same-day batch articles (thanks to @lamescholar for the report)
|
||||||
|
- (fix) showing login page in the selected theme (thanks to @feddiriko for the report)
|
||||||
|
- (fix) parsing atom feeds with html elements (thanks to @tillcash & @toBeOfUse for the report, @krkk for the fix)
|
||||||
|
- (fix) parsing feeds with missing guids (thanks to @hoyii for the report)
|
||||||
|
- (fix) sending actual client version to servers (thanks to @aidanholm)
|
||||||
|
- (fix) error caused by missing config dir (thanks to @timster)
|
||||||
|
- (etc) load external images with no-referrer policy (thanks to @tillcash for the report)
|
||||||
|
- (etc) open external links with no-referrer policy (thanks to @donovanglover)
|
||||||
|
- (etc) show article content in the list if title is missing (thanks to @asimpson for suggestion)
|
||||||
|
|
||||||
|
# v2.4 (2023-08-15)
|
||||||
|
|
||||||
- (new) ARM build support (thanks to @tillcash & @fenuks)
|
- (new) ARM build support (thanks to @tillcash & @fenuks)
|
||||||
- (new) auth configuration via param or env variable (thanks to @pierreprinetti)
|
- (new) auth configuration via param or env variable (thanks to @pierreprinetti)
|
||||||
|
|||||||
19
doc/fever.md
Normal file
19
doc/fever.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Fever API support
|
||||||
|
|
||||||
|
Fever API is a kind of RSS HTTP API interface, because the Fever API definition is not very clear, so the implementation of Fever server and Client may have some compatibility problems.
|
||||||
|
|
||||||
|
The Fever API implemented by Yarr is based on the Fever API spec: https://github.com/DigitalDJ/tinytinyrss-fever-plugin/blob/master/fever-api.md.
|
||||||
|
|
||||||
|
Here are some Apps that have been tested to work with yarr. Feel free to test other Clients/Apps and update the list here.
|
||||||
|
|
||||||
|
> Different apps support different URL/Address formats. Please note whether the URL entered has `http://` scheme and `/` suffix.
|
||||||
|
|
||||||
|
| App | Platforms | Config Server URL |
|
||||||
|
|:------------------------------------------------------------------------- | ---------------- |:--------------------------------------------------- |
|
||||||
|
| [Reeder](https://reederapp.com/) | MacOS<br>iOS | 127.0.0.1:7070/fever<br>http://127.0.0.1:7070/fever |
|
||||||
|
| [ReadKit](https://readkit.app/) | MacOS<br>iOS | http://127.0.0.1:7070/fever |
|
||||||
|
| [Fluent Reader](https://github.com/yang991178/fluent-reader) | MacOS<br>Windows | http://127.0.0.1:7070/fever/ |
|
||||||
|
| [Unread](https://apps.apple.com/us/app/unread-an-rss-reader/id1363637349) | iOS | http://127.0.0.1:7070/fever |
|
||||||
|
| [Fiery Feeds](https://voidstern.net/fiery-feeds) | MacOS<br>iOS | http://127.0.0.1:7070/fever |
|
||||||
|
|
||||||
|
If you are having trouble using Fever, please open an issue and @icefed, thanks.
|
||||||
68
doc/samples.yml
Normal file
68
doc/samples.yml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
- site: https://vimeo.com/channels/staffpicks/videos
|
||||||
|
feed: https://vimeo.com/channels/staffpicks/videos/rss
|
||||||
|
tags: [vimeo, image]
|
||||||
|
|
||||||
|
- site: https://www.youtube.com/@everyframeapainting/videos
|
||||||
|
feed: https://www.youtube.com/feeds/videos.xml?channel_id=UCjFqcJQXGZ6T6sxyFB-5i6A"
|
||||||
|
tags: [youtube, image]
|
||||||
|
|
||||||
|
- site: https://iwdrm.tumblr.com/
|
||||||
|
feed: https://iwdrm.tumblr.com/rss
|
||||||
|
tags: [tumblr, image]
|
||||||
|
|
||||||
|
- site: https://falseknees.tumblr.com/
|
||||||
|
feed: https://falseknees.tumblr.com/rss
|
||||||
|
tags: [tumblr, image]
|
||||||
|
|
||||||
|
- site: https://accidentallyquadratic.tumblr.com/
|
||||||
|
feed: https://accidentallyquadratic.tumblr.com/rss
|
||||||
|
info: text blog with code sections
|
||||||
|
tags: [tumblr, text, code]
|
||||||
|
|
||||||
|
- site: https://www.flickr.com/photos/maratsafin/
|
||||||
|
feed: https://www.flickr.com/services/feeds/photos_public.gne?id=59021497@N07&lang=en-us&format=atom
|
||||||
|
tags: [flickr, image]
|
||||||
|
|
||||||
|
- site: https://www.reddit.com/r/comics
|
||||||
|
feed: https://www.reddit.com/r/comics.rss
|
||||||
|
tags: [reddit, image]
|
||||||
|
|
||||||
|
- site: https://www.reddit.com/r/AITAH
|
||||||
|
feed: https://www.reddit.com/r/AITAH.rss
|
||||||
|
tags: [reddit, text]
|
||||||
|
|
||||||
|
- site: https://idothei.wordpress.com/
|
||||||
|
feed: https://idothei.wordpress.com/feed/
|
||||||
|
tags: [wordpress, text]
|
||||||
|
|
||||||
|
- site: https://www.vidarholen.net/contents/blog/
|
||||||
|
feed: https://www.vidarholen.net/contents/blog/?feed=rss2
|
||||||
|
tags: [wordpress, text]
|
||||||
|
|
||||||
|
- site: https://blog.posthaven.com/
|
||||||
|
feed: https://blog.posthaven.com/posts.atom
|
||||||
|
tags: [posthaven, text]
|
||||||
|
|
||||||
|
- site: https://medium.com/@dailynewsletter
|
||||||
|
feed: https://medium.com/feed/@dailynewsletter
|
||||||
|
tags: [medium, text]
|
||||||
|
|
||||||
|
- site: https://thereveal.substack.com/
|
||||||
|
feed: https://thereveal.substack.com/feed
|
||||||
|
tags: [substack, text]
|
||||||
|
|
||||||
|
- site: https://tema.livejournal.com/
|
||||||
|
feed: https://tema.livejournal.com/data/rss
|
||||||
|
tags: [livejournal, text]
|
||||||
|
|
||||||
|
- site: https://mametter.hatenablog.com/
|
||||||
|
feed: https://mametter.hatenablog.com/feed
|
||||||
|
tags: [hatena, text]
|
||||||
|
|
||||||
|
- site: https://juliepowell.blogspot.com/
|
||||||
|
feed: https://juliepowell.blogspot.com/feeds/posts/default
|
||||||
|
tags: [blogger, text]
|
||||||
|
|
||||||
|
- site: https://micro.blog/val
|
||||||
|
feed: https://micro.blog/posts/val
|
||||||
|
tags: [json, microblog]
|
||||||
12
dockerfile
12
dockerfile
@@ -1,12 +0,0 @@
|
|||||||
FROM golang:alpine AS build
|
|
||||||
RUN apk add build-base git
|
|
||||||
WORKDIR /src
|
|
||||||
COPY . .
|
|
||||||
RUN make build_linux
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
RUN apk add --no-cache ca-certificates && \
|
|
||||||
update-ca-certificates
|
|
||||||
COPY --from=build /src/_output/linux/yarr /usr/local/bin/yarr
|
|
||||||
EXPOSE 7070
|
|
||||||
CMD ["/usr/local/bin/yarr", "-addr", "0.0.0.0:7070", "-db", "/data/yarr.db"]
|
|
||||||
14
etc/dockerfile
Normal file
14
etc/dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM golang:alpine3.18 AS build
|
||||||
|
RUN apk add build-base git
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/root/go/pkg \
|
||||||
|
make host
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk add --no-cache ca-certificates && \
|
||||||
|
update-ca-certificates
|
||||||
|
COPY --from=build /src/out/yarr /usr/local/bin/yarr
|
||||||
|
EXPOSE 7070
|
||||||
|
ENTRYPOINT ["/usr/local/bin/yarr"]
|
||||||
|
CMD ["-addr", "0.0.0.0:7070", "-db", "/data/yarr.db"]
|
||||||
@@ -27,18 +27,12 @@ RUN env \
|
|||||||
CC=aarch64-linux-gnu-gcc \
|
CC=aarch64-linux-gnu-gcc \
|
||||||
CGO_ENABLED=1 \
|
CGO_ENABLED=1 \
|
||||||
GOOS=linux GOARCH=arm64 \
|
GOOS=linux GOARCH=arm64 \
|
||||||
go build \
|
make host && mv out/yarr /root/out/yarr.arm64
|
||||||
-tags "sqlite_foreign_keys release linux" \
|
|
||||||
-ldflags="-s -w" \
|
|
||||||
-o /root/out/yarr.arm64 src/main.go
|
|
||||||
|
|
||||||
RUN env \
|
RUN env \
|
||||||
CC=arm-linux-gnueabihf-gcc \
|
CC=arm-linux-gnueabihf-gcc \
|
||||||
CGO_ENABLED=1 \
|
CGO_ENABLED=1 \
|
||||||
GOOS=linux GOARCH=arm GOARM=7 \
|
GOOS=linux GOARCH=arm GOARM=7 \
|
||||||
go build \
|
make host && mv out/yarr /root/out/yarr.armv7
|
||||||
-tags "sqlite_foreign_keys release linux" \
|
|
||||||
-ldflags="-s -w" \
|
|
||||||
-o /root/out/yarr.arm7 src/main.go
|
|
||||||
|
|
||||||
CMD ["/bin/bash"]
|
CMD ["/bin/bash"]
|
||||||
BIN
etc/icon.icns
Normal file
BIN
etc/icon.icns
Normal file
Binary file not shown.
BIN
etc/icon_macos.png
Normal file
BIN
etc/icon_macos.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -1,5 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ ! -d "$HOME/.local/share/applications" ]]; then
|
||||||
|
mkdir -p "$HOME/.local/share/applications"
|
||||||
|
fi
|
||||||
|
|
||||||
cat >"$HOME/.local/share/applications/yarr.desktop" <<END
|
cat >"$HOME/.local/share/applications/yarr.desktop" <<END
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=yarr
|
Name=yarr
|
||||||
@@ -9,6 +13,10 @@ Type=Application
|
|||||||
Categories=Internet;
|
Categories=Internet;
|
||||||
END
|
END
|
||||||
|
|
||||||
|
if [[ ! -d "$HOME/.local/share/icons" ]]; then
|
||||||
|
mkdir -p "$HOME/.local/share/icons"
|
||||||
|
fi
|
||||||
|
|
||||||
cat >"$HOME/.local/share/icons/yarr.svg" <<END
|
cat >"$HOME/.local/share/icons/yarr.svg" <<END
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor-favicon">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor-favicon">
|
||||||
|
|||||||
62
etc/macos_package.sh
Executable file
62
etc/macos_package.sh
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
#/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "usage: $0 VERSION path/to/icon.icns path/to/binary output/dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
usage
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
ICNFILE=$2
|
||||||
|
BINFILE=$3
|
||||||
|
OUTPATH=$4
|
||||||
|
|
||||||
|
mkdir -p $OUTPATH/yarr.app/Contents/MacOS
|
||||||
|
mkdir -p $OUTPATH/yarr.app/Contents/Resources
|
||||||
|
|
||||||
|
mv $BINFILE $OUTPATH/yarr.app/Contents/MacOS/yarr
|
||||||
|
cp $ICNFILE $OUTPATH/yarr.app/Contents/Resources/icon.icns
|
||||||
|
|
||||||
|
chmod u+x $OUTPATH/yarr.app/Contents/MacOS/yarr
|
||||||
|
|
||||||
|
echo -n 'APPL????' >$OUTPATH/yarr.app/Contents/PkgInfo
|
||||||
|
cat <<EOF >$OUTPATH/yarr.app/Contents/Info.plist
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>yarr</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>yarr</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>nkanaev.yarr</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$VERSION</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>yarr</string>
|
||||||
|
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icon</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.news</string>
|
||||||
|
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>True</string>
|
||||||
|
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13</string>
|
||||||
|
<key>LSUIElement</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>Copyright © 2020 nkanaev. All rights reserved.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
BIN
etc/promo.png
BIN
etc/promo.png
Binary file not shown.
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 173 KiB |
89
etc/windows_versioninfo.sh
Executable file
89
etc/windows_versioninfo.sh
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Function to display usage information
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [-version VERSION] [-outfile FILENAME]"
|
||||||
|
echo " -version VERSION Set the version number (default: 0.0)"
|
||||||
|
echo " -outfile FILENAME Set the output file name (default: versioninfo.rc)"
|
||||||
|
echo ""
|
||||||
|
echo "This script generates a Windows resource file with version information."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
version="0.0"
|
||||||
|
outfile="versioninfo.rc"
|
||||||
|
|
||||||
|
# Check if help is requested
|
||||||
|
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse command-line options
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-version)
|
||||||
|
if [[ -z "$2" || "$2" == -* ]]; then
|
||||||
|
echo "Error: Missing value for -version parameter"
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
version="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-outfile)
|
||||||
|
if [[ -z "$2" || "$2" == -* ]]; then
|
||||||
|
echo "Error: Missing value for -outfile parameter"
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
outfile="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown parameter: $1"
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Replace dots with commas for version_comma
|
||||||
|
version_comma="${version//./,}"
|
||||||
|
|
||||||
|
# Use a here document for the template with ENDFILE delimiter
|
||||||
|
cat <<ENDFILE > "$outfile"
|
||||||
|
1 VERSIONINFO
|
||||||
|
FILEVERSION $version_comma,0,0
|
||||||
|
PRODUCTVERSION $version_comma,0,0
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "080904E4"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "Old MacDonald's Farm"
|
||||||
|
VALUE "FileDescription", "Yet another RSS reader"
|
||||||
|
VALUE "FileVersion", "$version"
|
||||||
|
VALUE "InternalName", "yarr"
|
||||||
|
VALUE "LegalCopyright", "nkanaev"
|
||||||
|
VALUE "OriginalFilename", "yarr.exe"
|
||||||
|
VALUE "ProductName", "yarr"
|
||||||
|
VALUE "ProductVersion", "$version"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x809, 1252
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
1 ICON "icon.ico"
|
||||||
|
ENDFILE
|
||||||
|
|
||||||
|
# Set the correct permissions
|
||||||
|
chmod 644 "$outfile"
|
||||||
|
|
||||||
|
echo "Generated $outfile with version $version"
|
||||||
8
go.mod
8
go.mod
@@ -1,11 +1,11 @@
|
|||||||
module github.com/nkanaev/yarr
|
module github.com/nkanaev/yarr
|
||||||
|
|
||||||
go 1.17
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mattn/go-sqlite3 v1.14.7
|
github.com/mattn/go-sqlite3 v1.14.7
|
||||||
golang.org/x/net v0.8.0
|
golang.org/x/net v0.36.0
|
||||||
golang.org/x/sys v0.6.0
|
golang.org/x/sys v0.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/text v0.8.0 // indirect
|
require golang.org/x/text v0.22.0 // indirect
|
||||||
|
|||||||
43
go.sum
43
go.sum
@@ -1,39 +1,8 @@
|
|||||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
|
|||||||
96
makefile
96
makefile
@@ -1,33 +1,89 @@
|
|||||||
VERSION=2.4
|
VERSION=2.4
|
||||||
GITHASH=$(shell git rev-parse --short=8 HEAD)
|
GITHASH=$(shell git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
CGO_ENABLED=1
|
GO_TAGS = sqlite_foreign_keys sqlite_json
|
||||||
|
GO_LDFLAGS = -s -w -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITHASH)'
|
||||||
|
|
||||||
GO_LDFLAGS = -s -w
|
GO_FLAGS = -tags "$(GO_TAGS)" -ldflags="$(GO_LDFLAGS)"
|
||||||
GO_LDFLAGS := $(GO_LDFLAGS) -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITHASH)'
|
GO_FLAGS_GUI = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS)"
|
||||||
|
GO_FLAGS_GUI_WIN = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS) -H windowsgui"
|
||||||
|
|
||||||
build_default:
|
export CGO_ENABLED=1
|
||||||
mkdir -p _output
|
|
||||||
go build -tags "sqlite_foreign_keys release" -ldflags="$(GO_LDFLAGS)" -o _output/yarr src/main.go
|
|
||||||
|
|
||||||
build_macos:
|
default: test host
|
||||||
mkdir -p _output/macos
|
|
||||||
GOOS=darwin GOARCH=amd64 go build -tags "sqlite_foreign_keys release macos" -ldflags="$(GO_LDFLAGS)" -o _output/macos/yarr src/main.go
|
|
||||||
cp src/platform/icon.png _output/macos/icon.png
|
|
||||||
go run bin/package_macos.go -outdir _output/macos -version "$(VERSION)"
|
|
||||||
|
|
||||||
build_linux:
|
# platform-specific files
|
||||||
mkdir -p _output/linux
|
|
||||||
GOOS=linux GOARCH=amd64 go build -tags "sqlite_foreign_keys release linux" -ldflags="$(GO_LDFLAGS)" -o _output/linux/yarr src/main.go
|
|
||||||
|
|
||||||
build_windows:
|
etc/icon.icns: etc/icon_macos.png
|
||||||
mkdir -p _output/windows
|
mkdir -p etc/icon.iconset
|
||||||
go run bin/generate_versioninfo.go -version "$(VERSION)" -outfile src/platform/versioninfo.rc
|
sips -s format png --resampleWidth 1024 etc/icon_macos.png --out etc/icon.iconset/icon_512x512@2x.png
|
||||||
|
sips -s format png --resampleWidth 512 etc/icon_macos.png --out etc/icon.iconset/icon_512x512.png
|
||||||
|
sips -s format png --resampleWidth 256 etc/icon_macos.png --out etc/icon.iconset/icon_256x256.png
|
||||||
|
sips -s format png --resampleWidth 128 etc/icon_macos.png --out etc/icon.iconset/icon_128x128.png
|
||||||
|
sips -s format png --resampleWidth 64 etc/icon_macos.png --out etc/icon.iconset/icon_32x32@2x.png
|
||||||
|
sips -s format png --resampleWidth 32 etc/icon_macos.png --out etc/icon.iconset/icon_32x32.png
|
||||||
|
sips -s format png --resampleWidth 16 etc/icon_macos.png --out etc/icon.iconset/icon_16x16.png
|
||||||
|
iconutil -c icns etc/icon.iconset -o etc/icon.icns
|
||||||
|
|
||||||
|
src/platform/versioninfo.rc:
|
||||||
|
./etc/windows_versioninfo.sh -version "$(VERSION)" -outfile src/platform/versioninfo.rc
|
||||||
windres -i src/platform/versioninfo.rc -O coff -o src/platform/versioninfo.syso
|
windres -i src/platform/versioninfo.rc -O coff -o src/platform/versioninfo.syso
|
||||||
GOOS=windows GOARCH=amd64 go build -tags "sqlite_foreign_keys release windows" -ldflags="$(GO_LDFLAGS) -H windowsgui" -o _output/windows/yarr.exe src/main.go
|
|
||||||
|
# build targets
|
||||||
|
|
||||||
|
host:
|
||||||
|
go build $(GO_FLAGS) -o out/yarr ./cmd/yarr
|
||||||
|
|
||||||
|
darwin_amd64:
|
||||||
|
# cross-compilation not supported: CC="zig cc -target x86_64-macos-none"
|
||||||
|
GOOS=darwin GOARCH=arm64 go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
|
||||||
|
|
||||||
|
darwin_arm64:
|
||||||
|
# cross-compilation not supported: CC="zig cc -target aarch64-macos-none"
|
||||||
|
GOOS=darwin GOARCH=arm64 go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
|
||||||
|
|
||||||
|
linux_amd64:
|
||||||
|
CC="zig cc -target x86_64-linux-musl -O2 -g0" CGO_CFLAGS="-D_LARGEFILE64_SOURCE" GOOS=linux GOARCH=amd64 \
|
||||||
|
go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
|
||||||
|
|
||||||
|
linux_arm64:
|
||||||
|
CC="zig cc -target aarch64-linux-musl -O2 -g0" CGO_CFLAGS="-D_LARGEFILE64_SOURCE" GOOS=linux GOARCH=arm64 \
|
||||||
|
go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
|
||||||
|
|
||||||
|
linux_armv7:
|
||||||
|
CC="zig cc -target arm-linux-musleabihf -O2 -g0" CGO_CFLAGS="-D_LARGEFILE64_SOURCE" GOOS=linux GOARCH=arm GOARM=7 \
|
||||||
|
go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
|
||||||
|
|
||||||
|
windows_amd64:
|
||||||
|
CC="zig cc -target x86_64-windows-gnu" GOOS=windows GOARCH=amd64 go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
|
||||||
|
|
||||||
|
windows_arm64:
|
||||||
|
CC="zig cc -target aarch64-windows-gnu" GOOS=windows GOARCH=arm64 go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
|
||||||
|
|
||||||
|
darwin_arm64_gui: etc/icon.icns
|
||||||
|
GOOS=darwin GOARCH=arm64 go build $(GO_FLAGS_GUI) -o out/$@/yarr ./cmd/yarr
|
||||||
|
./etc/macos_package.sh $(VERSION) etc/icon.icns out/$@/yarr out/$@
|
||||||
|
|
||||||
|
darwin_amd64_gui: etc/icon.icns
|
||||||
|
GOOS=darwin GOARCH=amd64 go build $(GO_FLAGS_GUI) -o out/$@/yarr ./cmd/yarr
|
||||||
|
./etc/macos_package.sh $(VERSION) etc/icon.icns out/$@/yarr out/$@
|
||||||
|
|
||||||
|
windows_amd64_gui: src/platform/versioninfo.rc
|
||||||
|
GOOS=windows GOARCH=amd64 go build $(GO_FLAGS_GUI_WIN) -o out/$@/yarr.exe ./cmd/yarr
|
||||||
|
|
||||||
|
windows_arm64_gui: src/platform/versioninfo.rc
|
||||||
|
GOOS=windows GOARCH=arm64 go build $(GO_FLAGS_GUI_WIN) -o out/$@/yarr.exe ./cmd/yarr
|
||||||
|
|
||||||
serve:
|
serve:
|
||||||
go run -tags "sqlite_foreign_keys" src/main.go -db local.db
|
go run $(GO_FLAGS) ./cmd/yarr -db local.db
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cd src && go test -tags "sqlite_foreign_keys release" ./...
|
go test $(GO_FLAGS) ./...
|
||||||
|
|
||||||
|
.PHONY: \
|
||||||
|
host \
|
||||||
|
darwin_amd64 darwin_amd64_gui \
|
||||||
|
darwin_arm64 darwin_arm64_gui \
|
||||||
|
windows_amd64 windows_amd64_gui \
|
||||||
|
windows_arm64 windows_arm64_gui \
|
||||||
|
serve test
|
||||||
|
|||||||
34
readme.md
34
readme.md
@@ -3,32 +3,36 @@
|
|||||||
**yarr** (yet another rss reader) is a web-based feed aggregator which can be used both
|
**yarr** (yet another rss reader) is a web-based feed aggregator which can be used both
|
||||||
as a desktop application and a personal self-hosted server.
|
as a desktop application and a personal self-hosted server.
|
||||||
|
|
||||||
It is written in Go with the frontend in Vue.js. The storage is backed by SQLite.
|
The app is a single binary with an embedded database (SQLite).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## usage
|
## usage
|
||||||
|
|
||||||
The latest prebuilt binaries for Linux/MacOS/Windows are available
|
The latest prebuilt binaries for Linux/MacOS/Windows AMD64 are available
|
||||||
[here](https://github.com/nkanaev/yarr/releases/latest).
|
[here](https://github.com/nkanaev/yarr/releases/latest). Installation instructions:
|
||||||
|
|
||||||
### macos
|
* MacOS
|
||||||
|
|
||||||
Download `yarr-*-macos64.zip`, unzip it, place `yarr.app` in `/Applications` folder, [open the app][macos-open], click the anchor menu bar icon, select "Open".
|
Download `yarr-*-macos64.zip`, unzip it, place `yarr.app` in `/Applications` folder, [open the app][macos-open], click the anchor menu bar icon, select "Open".
|
||||||
|
|
||||||
|
* Windows
|
||||||
|
|
||||||
|
Download `yarr-*-windows64.zip`, unzip it, open `yarr.exe`, click the anchor system tray icon, select "Open".
|
||||||
|
|
||||||
|
* Linux
|
||||||
|
|
||||||
|
Download `yarr-*-linux64.zip`, unzip it, place `yarr` in `$HOME/.local/bin`
|
||||||
|
and run [the script](etc/install-linux.sh).
|
||||||
|
|
||||||
[macos-open]: https://support.apple.com/en-gb/guide/mac-help/mh40616/mac
|
[macos-open]: https://support.apple.com/en-gb/guide/mac-help/mh40616/mac
|
||||||
|
|
||||||
### windows
|
|
||||||
|
|
||||||
Download `yarr-*-windows64.zip`, unzip it, open `yarr.exe`, click the anchor system tray icon, select "Open".
|
|
||||||
|
|
||||||
### linux
|
|
||||||
|
|
||||||
Download `yarr-*-linux64.zip`, unzip it, place `yarr` in `$HOME/.local/bin`
|
|
||||||
and run [the script](etc/install-linux.sh).
|
|
||||||
|
|
||||||
For self-hosting, see `yarr -h` for auth, tls & server configuration flags.
|
For self-hosting, see `yarr -h` for auth, tls & server configuration flags.
|
||||||
For building from source code, see [build.md](build.md)
|
|
||||||
|
See more:
|
||||||
|
|
||||||
|
* [Building from source code](doc/build.md)
|
||||||
|
* [Fever API support](doc/fever.md)
|
||||||
|
|
||||||
## credits
|
## credits
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
//go:build release
|
|
||||||
// +build release
|
|
||||||
|
|
||||||
package assets
|
package assets
|
||||||
|
|
||||||
import "embed"
|
import "embed"
|
||||||
|
|||||||
@@ -25,18 +25,21 @@
|
|||||||
<div class="flex-grow-1"></div>
|
<div class="flex-grow-1"></div>
|
||||||
<button class="toolbar-item"
|
<button class="toolbar-item"
|
||||||
:class="{active: filterSelected == 'unread'}"
|
:class="{active: filterSelected == 'unread'}"
|
||||||
|
:aria-pressed="filterSelected == 'unread'"
|
||||||
title="Unread"
|
title="Unread"
|
||||||
@click="filterSelected = 'unread'">
|
@click="filterSelected = 'unread'">
|
||||||
<span class="icon">{% inline "circle-full.svg" %}</span>
|
<span class="icon">{% inline "circle-full.svg" %}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="toolbar-item"
|
<button class="toolbar-item"
|
||||||
:class="{active: filterSelected == 'starred'}"
|
:class="{active: filterSelected == 'starred'}"
|
||||||
|
:aria-pressed="filterSelected == 'starred'"
|
||||||
title="Starred"
|
title="Starred"
|
||||||
@click="filterSelected = 'starred'">
|
@click="filterSelected = 'starred'">
|
||||||
<span class="icon">{% inline "star-full.svg" %}</span>
|
<span class="icon">{% inline "star-full.svg" %}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="toolbar-item"
|
<button class="toolbar-item"
|
||||||
:class="{active: filterSelected == ''}"
|
:class="{active: filterSelected == ''}"
|
||||||
|
:aria-pressed="filterSelected == ''"
|
||||||
title="All"
|
title="All"
|
||||||
@click="filterSelected = ''">
|
@click="filterSelected = ''">
|
||||||
<span class="icon">{% inline "assorted.svg" %}</span>
|
<span class="icon">{% inline "assorted.svg" %}</span>
|
||||||
@@ -59,10 +62,12 @@
|
|||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
|
|
||||||
<header class="dropdown-header">Theme</header>
|
<header class="dropdown-header" role="heading" aria-level="2">Theme</header>
|
||||||
<div class="row text-center m-0">
|
<div class="row text-center m-0">
|
||||||
<button class="btn btn-link col-4 px-0 rounded-0"
|
<button class="btn btn-link col-4 px-0 rounded-0"
|
||||||
:class="'theme-'+t"
|
:class="'theme-'+t"
|
||||||
|
:aria-label="t"
|
||||||
|
:aria-pressed="theme.name == t"
|
||||||
@click.stop="theme.name = t"
|
@click.stop="theme.name = t"
|
||||||
v-for="t in ['light', 'sepia', 'night']">
|
v-for="t in ['light', 'sepia', 'night']">
|
||||||
<span class="icon" v-if="theme.name == t">{% inline "check.svg" %}</span>
|
<span class="icon" v-if="theme.name == t">{% inline "check.svg" %}</span>
|
||||||
@@ -71,25 +76,25 @@
|
|||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
|
|
||||||
<header class="dropdown-header">Auto Refresh</header>
|
<header class="dropdown-header" role="heading" aria-level="2">Auto Refresh</header>
|
||||||
<div class="row text-center m-0">
|
<div class="row text-center m-0">
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: !refreshRate}" @click.stop="refreshRate = 0">0</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="!refreshRate" :class="{active: !refreshRate}" @click.stop="refreshRate = 0">0</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 10}" @click.stop="refreshRate = 10">10m</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 10" :class="{active: refreshRate == 10}" @click.stop="refreshRate = 10">10m</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 30}" @click.stop="refreshRate = 30">30m</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 30" :class="{active: refreshRate == 30}" @click.stop="refreshRate = 30">30m</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 60}" @click.stop="refreshRate = 60">1h</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 60" :class="{active: refreshRate == 60}" @click.stop="refreshRate = 60">1h</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 120}" @click.stop="refreshRate = 120">2h</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 120" :class="{active: refreshRate == 120}" @click.stop="refreshRate = 120">2h</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 240}" @click.stop="refreshRate = 240">4h</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 240" :class="{active: refreshRate == 240}" @click.stop="refreshRate = 240">4h</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
|
|
||||||
<header class="dropdown-header">Show first</header>
|
<header class="dropdown-header" role="heading" aria-level="2">Show first</header>
|
||||||
<div class="d-flex text-center">
|
<div class="d-flex text-center">
|
||||||
<button class="dropdown-item px-0" :class="{active: itemSortNewestFirst}" @click.stop="itemSortNewestFirst=true">New</button>
|
<button class="dropdown-item px-0" :aria-pressed="itemSortNewestFirst" :class="{active: itemSortNewestFirst}" @click.stop="itemSortNewestFirst=true">New</button>
|
||||||
<button class="dropdown-item px-0" :class="{active: !itemSortNewestFirst}" @click.stop="itemSortNewestFirst=false">Old</button>
|
<button class="dropdown-item px-0" :aria-pressed="!itemSortNewestFirst" :class="{active: !itemSortNewestFirst}" @click.stop="itemSortNewestFirst=false">Old</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<header class="dropdown-header">Subscriptions</header>
|
<header class="dropdown-header" role="heading" aria-level="2">Subscriptions</header>
|
||||||
<form id="opml-import-form" enctype="multipart/form-data" tabindex="-1">
|
<form id="opml-import-form" enctype="multipart/form-data" tabindex="-1">
|
||||||
<input type="file"
|
<input type="file"
|
||||||
id="opml-import"
|
id="opml-import"
|
||||||
@@ -206,12 +211,12 @@
|
|||||||
<template v-slot:button>
|
<template v-slot:button>
|
||||||
<span class="icon">{% inline "more-horizontal.svg" %}</span>
|
<span class="icon">{% inline "more-horizontal.svg" %}</span>
|
||||||
</template>
|
</template>
|
||||||
<header class="dropdown-header">{{ current.feed.title }}</header>
|
<header class="dropdown-header" role="heading" aria-level="2">{{ current.feed.title }}</header>
|
||||||
<a class="dropdown-item" :href="current.feed.link" target="_blank" v-if="current.feed.link">
|
<a class="dropdown-item" :href="current.feed.link" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer" v-if="current.feed.link">
|
||||||
<span class="icon mr-1">{% inline "globe.svg" %}</span>
|
<span class="icon mr-1">{% inline "globe.svg" %}</span>
|
||||||
Website
|
Website
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" :href="current.feed.feed_link" target="_blank" v-if="current.feed.feed_link">
|
<a class="dropdown-item" :href="current.feed.feed_link" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer" v-if="current.feed.feed_link">
|
||||||
<span class="icon mr-1">{% inline "rss.svg" %}</span>
|
<span class="icon mr-1">{% inline "rss.svg" %}</span>
|
||||||
Feed Link
|
Feed Link
|
||||||
</a>
|
</a>
|
||||||
@@ -220,8 +225,12 @@
|
|||||||
<span class="icon mr-1">{% inline "edit.svg" %}</span>
|
<span class="icon mr-1">{% inline "edit.svg" %}</span>
|
||||||
Rename
|
Rename
|
||||||
</button>
|
</button>
|
||||||
|
<button class="dropdown-item" @click="updateFeedLink(current.feed)" v-if="current.feed.feed_link">
|
||||||
|
<span class="icon mr-1">{% inline "edit.svg" %}</span>
|
||||||
|
Change Link
|
||||||
|
</button>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<header class="dropdown-header">Move to...</header>
|
<header class="dropdown-header" role="heading" aria-level="2">Move to...</header>
|
||||||
<button class="dropdown-item"
|
<button class="dropdown-item"
|
||||||
v-if="folder.id != current.feed.folder_id"
|
v-if="folder.id != current.feed.folder_id"
|
||||||
v-for="folder in folders"
|
v-for="folder in folders"
|
||||||
@@ -251,7 +260,7 @@
|
|||||||
<template v-slot:button>
|
<template v-slot:button>
|
||||||
<span class="icon">{% inline "more-horizontal.svg" %}</span>
|
<span class="icon">{% inline "more-horizontal.svg" %}</span>
|
||||||
</template>
|
</template>
|
||||||
<header class="dropdown-header">{{ current.folder.title }}</header>
|
<header class="dropdown-header" role="heading" aria-level="2">{{ current.folder.title }}</header>
|
||||||
<button class="dropdown-item" @click="renameFolder(current.folder)">
|
<button class="dropdown-item" @click="renameFolder(current.folder)">
|
||||||
<span class="icon mr-1">{% inline "edit.svg" %}</span>
|
<span class="icon mr-1">{% inline "edit.svg" %}</span>
|
||||||
Rename
|
Rename
|
||||||
@@ -322,10 +331,16 @@
|
|||||||
title="Read Here">
|
title="Read Here">
|
||||||
<span class="icon" :class="{'icon-loading': loading.readability}">{% inline "book-open.svg" %}</span>
|
<span class="icon" :class="{'icon-loading': loading.readability}">{% inline "book-open.svg" %}</span>
|
||||||
</button>
|
</button>
|
||||||
<a class="toolbar-item" :href="itemSelectedDetails.link" target="_blank" title="Open Link">
|
<a class="toolbar-item" :href="itemSelectedDetails.link" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer" title="Open Link">
|
||||||
<span class="icon">{% inline "external-link.svg" %}</span>
|
<span class="icon">{% inline "external-link.svg" %}</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex-grow-1"></div>
|
<div class="flex-grow-1"></div>
|
||||||
|
<button class="toolbar-item" @click="navigateToItem(-1)" title="Previous Article" :disabled="itemSelected == items[0].id">
|
||||||
|
<span class="icon">{% inline "chevron-left.svg" %}</span>
|
||||||
|
</button>
|
||||||
|
<button class="toolbar-item" @click="navigateToItem(+1)" title="Next Article" :disabled="itemSelected == items[items.length - 1].id">
|
||||||
|
<span class="icon">{% inline "chevron-right.svg" %}</span>
|
||||||
|
</button>
|
||||||
<button class="toolbar-item" @click="itemSelected=null" title="Close Article">
|
<button class="toolbar-item" @click="itemSelected=null" title="Close Article">
|
||||||
<span class="icon">{% inline "x.svg" %}</span>
|
<span class="icon">{% inline "x.svg" %}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -338,13 +353,23 @@
|
|||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
<h1><b>{{ itemSelectedDetails.title || 'untitled' }}</b></h1>
|
<h1><b>{{ itemSelectedDetails.title || 'untitled' }}</b></h1>
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
<div>{{ (feedsById[itemSelectedDetails.feed_id] || {}).title }}</div>
|
<div>
|
||||||
|
<span class="cursor-pointer" @click="feedSelected = 'feed:'+(feedsById[itemSelectedDetails.feed_id] || {}).id">
|
||||||
|
{{ (feedsById[itemSelectedDetails.feed_id] || {}).title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<time>{{ formatDate(itemSelectedDetails.date) }}</time>
|
<time>{{ formatDate(itemSelectedDetails.date) }}</time>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div v-if="!itemSelectedReadability">
|
<div v-if="!itemSelectedReadability">
|
||||||
<img :src="itemSelectedDetails.image" v-if="itemSelectedDetails.image" class="mb-3">
|
<div v-if="contentImages.length">
|
||||||
<audio class="w-100" controls v-if="itemSelectedDetails.podcast_url" :src="itemSelectedDetails.podcast_url"></audio>
|
<figure v-for="media in contentImages">
|
||||||
|
<img :src="media.url" loading="lazy">
|
||||||
|
<figcaption v-if="media.description" v-html="media.description"></figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<audio class="w-100" controls v-for="media in contentAudios" :src="media.url"></audio>
|
||||||
|
<video class="w-100" controls v-for="media in contentVideos" :src="media.url"></video>
|
||||||
</div>
|
</div>
|
||||||
<div v-html="itemSelectedContent"></div>
|
<div v-html="itemSelectedContent"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,26 @@
|
|||||||
|
|
||||||
var TITLE = document.title
|
var TITLE = document.title
|
||||||
|
|
||||||
|
function scrollto(target, scroll) {
|
||||||
|
var padding = 10
|
||||||
|
var targetRect = target.getBoundingClientRect()
|
||||||
|
var scrollRect = scroll.getBoundingClientRect()
|
||||||
|
|
||||||
|
// target
|
||||||
|
var relativeOffset = targetRect.y - scrollRect.y
|
||||||
|
var absoluteOffset = relativeOffset + scroll.scrollTop
|
||||||
|
|
||||||
|
if (padding <= relativeOffset && relativeOffset + targetRect.height <= scrollRect.height - padding) return
|
||||||
|
|
||||||
|
var newPos = scroll.scrollTop
|
||||||
|
if (relativeOffset < padding) {
|
||||||
|
newPos = absoluteOffset - padding
|
||||||
|
} else {
|
||||||
|
newPos = absoluteOffset - scrollRect.height + targetRect.height + padding
|
||||||
|
}
|
||||||
|
scroll.scrollTop = Math.round(newPos)
|
||||||
|
}
|
||||||
|
|
||||||
var debounce = function(callback, wait) {
|
var debounce = function(callback, wait) {
|
||||||
var timeout
|
var timeout
|
||||||
return function() {
|
return function() {
|
||||||
@@ -278,6 +298,18 @@ var vm = new Vue({
|
|||||||
|
|
||||||
return this.itemSelectedDetails.content || ''
|
return this.itemSelectedDetails.content || ''
|
||||||
},
|
},
|
||||||
|
contentImages: function() {
|
||||||
|
if (!this.itemSelectedDetails) return []
|
||||||
|
return (this.itemSelectedDetails.media_links || []).filter(l => l.type === 'image')
|
||||||
|
},
|
||||||
|
contentAudios: function() {
|
||||||
|
if (!this.itemSelectedDetails) return []
|
||||||
|
return (this.itemSelectedDetails.media_links || []).filter(l => l.type === 'audio')
|
||||||
|
},
|
||||||
|
contentVideos: function() {
|
||||||
|
if (!this.itemSelectedDetails) return []
|
||||||
|
return (this.itemSelectedDetails.media_links || []).filter(l => l.type === 'video')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'theme': {
|
'theme': {
|
||||||
@@ -407,7 +439,7 @@ var vm = new Vue({
|
|||||||
vm.feeds = values[1]
|
vm.feeds = values[1]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
refreshItems: function(loadMore) {
|
refreshItems: function(loadMore = false) {
|
||||||
if (this.feedSelected === null) {
|
if (this.feedSelected === null) {
|
||||||
vm.items = []
|
vm.items = []
|
||||||
vm.itemsHasMore = false
|
vm.itemsHasMore = false
|
||||||
@@ -420,7 +452,7 @@ var vm = new Vue({
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.loading.items = true
|
this.loading.items = true
|
||||||
api.items.list(query).then(function(data) {
|
return api.items.list(query).then(function(data) {
|
||||||
if (loadMore) {
|
if (loadMore) {
|
||||||
vm.items = vm.items.concat(data.list)
|
vm.items = vm.items.concat(data.list)
|
||||||
} else {
|
} else {
|
||||||
@@ -443,13 +475,17 @@ var vm = new Vue({
|
|||||||
var scale = (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16) / 16
|
var scale = (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16) / 16
|
||||||
|
|
||||||
var el = this.$refs.itemlist
|
var el = this.$refs.itemlist
|
||||||
|
|
||||||
|
if (el.scrollHeight === 0) return false // element is invisible (responsive design)
|
||||||
|
|
||||||
var closeToBottom = (el.scrollHeight - el.scrollTop - el.offsetHeight) < bottomSpace * scale
|
var closeToBottom = (el.scrollHeight - el.scrollTop - el.offsetHeight) < bottomSpace * scale
|
||||||
return closeToBottom
|
return closeToBottom
|
||||||
},
|
},
|
||||||
loadMoreItems: function(event, el) {
|
loadMoreItems: function(event, el) {
|
||||||
if (!this.itemsHasMore) return
|
if (!this.itemsHasMore) return
|
||||||
if (this.loading.items) return
|
if (this.loading.items) return
|
||||||
if (this.itemListCloseToBottom()) this.refreshItems(true)
|
if (this.itemListCloseToBottom()) return this.refreshItems(true)
|
||||||
|
if (this.itemSelected && this.itemSelected === this.items[this.items.length - 1].id) return this.refreshItems(true)
|
||||||
},
|
},
|
||||||
markItemsRead: function() {
|
markItemsRead: function() {
|
||||||
var query = this.getItemsQuery()
|
var query = this.getItemsQuery()
|
||||||
@@ -523,6 +559,14 @@ var vm = new Vue({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateFeedLink: function(feed) {
|
||||||
|
var newLink = prompt('Enter feed link', feed.feed_link)
|
||||||
|
if (newLink) {
|
||||||
|
api.feeds.update(feed.id, {feed_link: newLink}).then(function() {
|
||||||
|
feed.feed_link = newLink
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
renameFeed: function(feed) {
|
renameFeed: function(feed) {
|
||||||
var newTitle = prompt('Enter new title', feed.title)
|
var newTitle = prompt('Enter new title', feed.title)
|
||||||
if (newTitle) {
|
if (newTitle) {
|
||||||
@@ -675,6 +719,65 @@ var vm = new Vue({
|
|||||||
this.filteredFolderStats = statsFolders
|
this.filteredFolderStats = statsFolders
|
||||||
this.filteredTotalStats = statsTotal
|
this.filteredTotalStats = statsTotal
|
||||||
},
|
},
|
||||||
|
// navigation helper, navigate relative to selected item
|
||||||
|
navigateToItem: function(relativePosition) {
|
||||||
|
let vm = this
|
||||||
|
if (vm.itemSelected == null) {
|
||||||
|
// if no item is selected, select first
|
||||||
|
if (vm.items.length !== 0) vm.itemSelected = vm.items[0].id
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemPosition = vm.items.findIndex(function(x) { return x.id === vm.itemSelected })
|
||||||
|
if (itemPosition === -1) {
|
||||||
|
if (vm.items.length !== 0) vm.itemSelected = vm.items[0].id
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPosition = itemPosition + relativePosition
|
||||||
|
if (newPosition < 0 || newPosition >= vm.items.length) return
|
||||||
|
|
||||||
|
vm.itemSelected = vm.items[newPosition].id
|
||||||
|
|
||||||
|
vm.$nextTick(function() {
|
||||||
|
var scroll = document.querySelector('#item-list-scroll')
|
||||||
|
|
||||||
|
var handle = scroll.querySelector('input[type=radio]:checked')
|
||||||
|
var target = handle && handle.parentElement
|
||||||
|
|
||||||
|
if (target && scroll) scrollto(target, scroll)
|
||||||
|
|
||||||
|
vm.loadMoreItems()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// navigation helper, navigate relative to selected feed
|
||||||
|
navigateToFeed: function(relativePosition) {
|
||||||
|
let vm = this
|
||||||
|
var navigationList = Array.from(document.querySelectorAll('#col-feed-list input[name=feed]'))
|
||||||
|
.filter(function(r) { return r.offsetParent !== null && r.value !== 'folder:null' })
|
||||||
|
.map(function(r) { return r.value })
|
||||||
|
|
||||||
|
var currentFeedPosition = navigationList.indexOf(vm.feedSelected)
|
||||||
|
|
||||||
|
if (currentFeedPosition == -1) {
|
||||||
|
vm.feedSelected = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPosition = currentFeedPosition+relativePosition
|
||||||
|
if (newPosition < 0 || newPosition >= navigationList.length) return
|
||||||
|
|
||||||
|
vm.feedSelected = navigationList[newPosition]
|
||||||
|
|
||||||
|
vm.$nextTick(function() {
|
||||||
|
var scroll = document.querySelector('#feed-list-scroll')
|
||||||
|
|
||||||
|
var handle = scroll.querySelector('input[type=radio]:checked')
|
||||||
|
var target = handle && handle.parentElement
|
||||||
|
|
||||||
|
if (target && scroll) scrollto(target, scroll)
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +1,4 @@
|
|||||||
function scrollto(target, scroll) {
|
|
||||||
var padding = 10
|
|
||||||
var targetRect = target.getBoundingClientRect()
|
|
||||||
var scrollRect = scroll.getBoundingClientRect()
|
|
||||||
|
|
||||||
// target
|
|
||||||
var relativeOffset = targetRect.y - scrollRect.y
|
|
||||||
var absoluteOffset = relativeOffset + scroll.scrollTop
|
|
||||||
|
|
||||||
if (padding <= relativeOffset && relativeOffset + targetRect.height <= scrollRect.height - padding) return
|
|
||||||
|
|
||||||
var newPos = scroll.scrollTop
|
|
||||||
if (relativeOffset < padding) {
|
|
||||||
newPos = absoluteOffset - padding
|
|
||||||
} else {
|
|
||||||
newPos = absoluteOffset - scrollRect.height + targetRect.height + padding
|
|
||||||
}
|
|
||||||
scroll.scrollTop = Math.round(newPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
var helperFunctions = {
|
var helperFunctions = {
|
||||||
// navigation helper, navigate relative to selected item
|
|
||||||
navigateToItem: function(relativePosition) {
|
|
||||||
if (vm.itemSelected == null) {
|
|
||||||
// if no item is selected, select first
|
|
||||||
if (vm.items.length !== 0) vm.itemSelected = vm.items[0].id
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemPosition = vm.items.findIndex(function(x) { return x.id === vm.itemSelected })
|
|
||||||
if (itemPosition === -1) {
|
|
||||||
if (vm.items.length !== 0) vm.itemSelected = vm.items[0].id
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPosition = itemPosition + relativePosition
|
|
||||||
if (newPosition < 0 || newPosition >= vm.items.length) return
|
|
||||||
|
|
||||||
vm.itemSelected = vm.items[newPosition].id
|
|
||||||
|
|
||||||
vm.$nextTick(function() {
|
|
||||||
var scroll = document.querySelector('#item-list-scroll')
|
|
||||||
|
|
||||||
var handle = scroll.querySelector('input[type=radio]:checked')
|
|
||||||
var target = handle && handle.parentElement
|
|
||||||
|
|
||||||
if (target && scroll) scrollto(target, scroll)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// navigation helper, navigate relative to selected feed
|
|
||||||
navigateToFeed: function(relativePosition) {
|
|
||||||
var navigationList = Array.from(document.querySelectorAll('#col-feed-list input[name=feed]'))
|
|
||||||
.filter(function(r) { return r.offsetParent !== null && r.value !== 'folder:null' })
|
|
||||||
.map(function(r) { return r.value })
|
|
||||||
|
|
||||||
var currentFeedPosition = navigationList.indexOf(vm.feedSelected)
|
|
||||||
|
|
||||||
if (currentFeedPosition == -1) {
|
|
||||||
vm.feedSelected = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPosition = currentFeedPosition+relativePosition
|
|
||||||
if (newPosition < 0 || newPosition >= navigationList.length) return
|
|
||||||
|
|
||||||
vm.feedSelected = navigationList[newPosition]
|
|
||||||
|
|
||||||
vm.$nextTick(function() {
|
|
||||||
var scroll = document.querySelector('#feed-list-scroll')
|
|
||||||
|
|
||||||
var handle = scroll.querySelector('input[type=radio]:checked')
|
|
||||||
var target = handle && handle.parentElement
|
|
||||||
|
|
||||||
if (target && scroll) scrollto(target, scroll)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
scrollContent: function(direction) {
|
scrollContent: function(direction) {
|
||||||
var padding = 40
|
var padding = 40
|
||||||
var scroll = document.querySelector('.content')
|
var scroll = document.querySelector('.content')
|
||||||
@@ -92,7 +17,7 @@ var helperFunctions = {
|
|||||||
var shortcutFunctions = {
|
var shortcutFunctions = {
|
||||||
openItemLink: function() {
|
openItemLink: function() {
|
||||||
if (vm.itemSelectedDetails && vm.itemSelectedDetails.link) {
|
if (vm.itemSelectedDetails && vm.itemSelectedDetails.link) {
|
||||||
window.open(vm.itemSelectedDetails.link, '_blank')
|
window.open(vm.itemSelectedDetails.link, '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleReadability: function() {
|
toggleReadability: function() {
|
||||||
@@ -118,16 +43,16 @@ var shortcutFunctions = {
|
|||||||
document.getElementById("searchbar").focus()
|
document.getElementById("searchbar").focus()
|
||||||
},
|
},
|
||||||
nextItem(){
|
nextItem(){
|
||||||
helperFunctions.navigateToItem(+1)
|
vm.navigateToItem(+1)
|
||||||
},
|
},
|
||||||
previousItem() {
|
previousItem() {
|
||||||
helperFunctions.navigateToItem(-1)
|
vm.navigateToItem(-1)
|
||||||
},
|
},
|
||||||
nextFeed(){
|
nextFeed(){
|
||||||
helperFunctions.navigateToFeed(+1)
|
vm.navigateToFeed(+1)
|
||||||
},
|
},
|
||||||
previousFeed() {
|
previousFeed() {
|
||||||
helperFunctions.navigateToFeed(-1)
|
vm.navigateToFeed(-1)
|
||||||
},
|
},
|
||||||
scrollForward: function() {
|
scrollForward: function() {
|
||||||
helperFunctions.scrollContent(+1)
|
helperFunctions.scrollContent(+1)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="theme-{% .settings.theme_name %}">
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
<img src="./static/graphicarts/anchor.svg" alt="">
|
<img src="./static/graphicarts/anchor.svg" alt="">
|
||||||
{% if .error %}
|
{% if .error %}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package htmlutil
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Any(els []string, el string, match func(string, string) bool) bool {
|
func Any(els []string, el string, match func(string, string) bool) bool {
|
||||||
@@ -31,3 +32,7 @@ func URLDomain(val string) string {
|
|||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsAPossibleLink(val string) bool {
|
||||||
|
return strings.HasPrefix(val, "http://") || strings.HasPrefix(val, "https://")
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
@@ -61,3 +62,16 @@ func ExtractText(content string) string {
|
|||||||
text = whitespaceRegex.ReplaceAllLiteralString(text, " ")
|
text = whitespaceRegex.ReplaceAllLiteralString(text, " ")
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TruncateText(input string, size int) string {
|
||||||
|
runes := []rune(input)
|
||||||
|
if len(runes) <= size {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
for i := size - 1; i > 0; i-- {
|
||||||
|
if unicode.IsSpace(runes[i]) {
|
||||||
|
return string(runes[:i]) + " ..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,3 +24,21 @@ func TestExtractText(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTruncateText(t *testing.T) {
|
||||||
|
input := "Lorem ipsum — классический текст-«рыба»"
|
||||||
|
|
||||||
|
size := 30
|
||||||
|
want := "Lorem ipsum — классический ..."
|
||||||
|
have := TruncateText(input, size)
|
||||||
|
if want != have {
|
||||||
|
t.Errorf("\nsize: %d\nwant: %#v\nhave: %#v", size, want, have)
|
||||||
|
}
|
||||||
|
|
||||||
|
size = 1000
|
||||||
|
want = input
|
||||||
|
have = TruncateText(input, size)
|
||||||
|
if want != have {
|
||||||
|
t.Errorf("\nsize: %d\nwant: %#v\nhave: %#v", size, want, have)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func getExtraAttributes(tagName string) ([]string, []string) {
|
|||||||
case "iframe":
|
case "iframe":
|
||||||
return []string{"sandbox", "loading"}, []string{`sandbox="allow-scripts allow-same-origin allow-popups"`, `loading="lazy"`}
|
return []string{"sandbox", "loading"}, []string{`sandbox="allow-scripts allow-same-origin allow-popups"`, `loading="lazy"`}
|
||||||
case "img":
|
case "img":
|
||||||
return []string{"loading"}, []string{`loading="lazy"`}
|
return []string{"loading"}, []string{`loading="lazy"`, `referrerpolicy="no-referrer"`}
|
||||||
default:
|
default:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import "testing"
|
|||||||
|
|
||||||
func TestValidInput(t *testing.T) {
|
func TestValidInput(t *testing.T) {
|
||||||
input := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy">.</p>`
|
input := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy">.</p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
want := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy" referrerpolicy="no-referrer">.</p>`
|
||||||
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if input != output {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
|
t.Errorf("Wrong output: \nwant: %#v\nhave: %#v", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,31 +28,31 @@ func TestImgWithTextDataURL(t *testing.T) {
|
|||||||
|
|
||||||
func TestImgWithDataURL(t *testing.T) {
|
func TestImgWithDataURL(t *testing.T) {
|
||||||
input := `<img src="data:image/gif;base64,test" alt="Example">`
|
input := `<img src="data:image/gif;base64,test" alt="Example">`
|
||||||
expected := `<img src="data:image/gif;base64,test" alt="Example" loading="lazy">`
|
want := `<img src="data:image/gif;base64,test" alt="Example" loading="lazy" referrerpolicy="no-referrer">`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if output != expected {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImgWithSrcset(t *testing.T) {
|
func TestImgWithSrcset(t *testing.T) {
|
||||||
input := `<img srcset="example-320w.jpg, example-480w.jpg 1.5x, example-640w.jpg 2x, example-640w.jpg 640w" src="example-640w.jpg" alt="Example">`
|
input := `<img srcset="example-320w.jpg, example-480w.jpg 1.5x, example-640w.jpg 2x, example-640w.jpg 640w" src="example-640w.jpg" alt="Example">`
|
||||||
expected := `<img srcset="http://example.org/example-320w.jpg, http://example.org/example-480w.jpg 1.5x, http://example.org/example-640w.jpg 2x, http://example.org/example-640w.jpg 640w" src="http://example.org/example-640w.jpg" alt="Example" loading="lazy">`
|
want := `<img srcset="http://example.org/example-320w.jpg, http://example.org/example-480w.jpg 1.5x, http://example.org/example-640w.jpg 2x, http://example.org/example-640w.jpg 640w" src="http://example.org/example-640w.jpg" alt="Example" loading="lazy" referrerpolicy="no-referrer">`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if output != expected {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImgWithSrcsetAndDataURL(t *testing.T) {
|
func TestImgWithSrcsetAndDataURL(t *testing.T) {
|
||||||
input := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example">`
|
input := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example">`
|
||||||
expected := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example" loading="lazy">`
|
want := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example" loading="lazy" referrerpolicy="no-referrer">`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if output != expected {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,16 +68,16 @@ func TestSourceWithSrcsetAndMedia(t *testing.T) {
|
|||||||
|
|
||||||
func TestMediumImgWithSrcset(t *testing.T) {
|
func TestMediumImgWithSrcset(t *testing.T) {
|
||||||
input := `<img alt="Image for post" class="t u v ef aj" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" width="2730" height="3407">`
|
input := `<img alt="Image for post" class="t u v ef aj" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" width="2730" height="3407">`
|
||||||
expected := `<img alt="Image for post" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" loading="lazy">`
|
want := `<img alt="Image for post" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" loading="lazy" referrerpolicy="no-referrer">`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if output != expected {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelfClosingTags(t *testing.T) {
|
func TestSelfClosingTags(t *testing.T) {
|
||||||
input := `<p>This <br> is a <strong>text</strong> <br/>with an image: <img src="http://example.org/" alt="Test" loading="lazy"/>.</p>`
|
input := `<p>This <br> is a <strong>text</strong><br/>.</p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if input != output {
|
if input != output {
|
||||||
@@ -95,11 +96,11 @@ func TestTable(t *testing.T) {
|
|||||||
|
|
||||||
func TestRelativeURL(t *testing.T) {
|
func TestRelativeURL(t *testing.T) {
|
||||||
input := `This <a href="/test.html">link is relative</a> and this image: <img src="../folder/image.png"/>`
|
input := `This <a href="/test.html">link is relative</a> and this image: <img src="../folder/image.png"/>`
|
||||||
expected := `This <a href="http://example.org/test.html" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">link is relative</a> and this image: <img src="http://example.org/folder/image.png" loading="lazy"/>`
|
want := `This <a href="http://example.org/test.html" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">link is relative</a> and this image: <img src="http://example.org/folder/image.png" loading="lazy" referrerpolicy="no-referrer"/>`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if want != have {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,11 +166,11 @@ func TestInvalidNestedTag(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidIFrame(t *testing.T) {
|
func TestValidIFrame(t *testing.T) {
|
||||||
input := `<iframe src="http://example.org/"></iframe>`
|
input := `<iframe src="http://example.org/"></iframe>`
|
||||||
expected := `<iframe src="http://example.org/" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
|
want := `<iframe src="http://example.org/" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if want != have {
|
||||||
t.Errorf("Wrong output:\nwant: %s\nhave: %s", expected, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package scraper
|
package scraper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nkanaev/yarr/src/content/htmlutil"
|
"github.com/nkanaev/yarr/src/content/htmlutil"
|
||||||
@@ -35,6 +36,18 @@ func FindFeeds(body string, base string) map[string]string {
|
|||||||
link := htmlutil.AbsoluteUrl(href, base)
|
link := htmlutil.AbsoluteUrl(href, base)
|
||||||
if link != "" {
|
if link != "" {
|
||||||
candidates[link] = name
|
candidates[link] = name
|
||||||
|
|
||||||
|
l, err := url.Parse(link)
|
||||||
|
if err == nil && l.Host == "www.youtube.com" && l.Path == "/feeds/videos.xml" {
|
||||||
|
// https://wiki.archiveteam.org/index.php/YouTube/Technical_details#Playlists
|
||||||
|
channelID, found := strings.CutPrefix(l.Query().Get("channel_id"), "UC")
|
||||||
|
if found {
|
||||||
|
const url string = "https://www.youtube.com/feeds/videos.xml?playlist_id="
|
||||||
|
candidates[url+"UULF"+channelID] = name + " - Videos"
|
||||||
|
candidates[url+"UULV"+channelID] = name + " - Live Streams"
|
||||||
|
candidates[url+"UUSH"+channelID] = name + " - Short videos"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"html"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -47,6 +46,8 @@ type atomLinks []atomLink
|
|||||||
func (a *atomText) Text() string {
|
func (a *atomText) Text() string {
|
||||||
if a.Type == "html" {
|
if a.Type == "html" {
|
||||||
return htmlutil.ExtractText(a.Data)
|
return htmlutil.ExtractText(a.Data)
|
||||||
|
} else if a.Type == "xhtml" {
|
||||||
|
return htmlutil.ExtractText(a.XML)
|
||||||
}
|
}
|
||||||
return a.Data
|
return a.Data
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,7 @@ func (a *atomText) String() string {
|
|||||||
if a.Type == "xhtml" {
|
if a.Type == "xhtml" {
|
||||||
data = a.XML
|
data = a.XML
|
||||||
}
|
}
|
||||||
return html.UnescapeString(strings.TrimSpace(data))
|
return strings.TrimSpace(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (links atomLinks) First(rel string) string {
|
func (links atomLinks) First(rel string) string {
|
||||||
@@ -81,15 +82,23 @@ func ParseAtom(r io.Reader) (*Feed, error) {
|
|||||||
SiteURL: firstNonEmpty(srcfeed.Links.First("alternate"), srcfeed.Links.First("")),
|
SiteURL: firstNonEmpty(srcfeed.Links.First("alternate"), srcfeed.Links.First("")),
|
||||||
}
|
}
|
||||||
for _, srcitem := range srcfeed.Entries {
|
for _, srcitem := range srcfeed.Entries {
|
||||||
link := firstNonEmpty(srcitem.OrigLink, srcitem.Links.First("alternate"), srcitem.Links.First(""))
|
linkFromID := ""
|
||||||
|
guidFromID := ""
|
||||||
|
if htmlutil.IsAPossibleLink(srcitem.ID) {
|
||||||
|
linkFromID = srcitem.ID
|
||||||
|
guidFromID = srcitem.ID + "::" + srcitem.Updated
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaLinks := srcitem.mediaLinks()
|
||||||
|
|
||||||
|
link := firstNonEmpty(srcitem.OrigLink, srcitem.Links.First("alternate"), srcitem.Links.First(""), linkFromID)
|
||||||
dstfeed.Items = append(dstfeed.Items, Item{
|
dstfeed.Items = append(dstfeed.Items, Item{
|
||||||
GUID: firstNonEmpty(srcitem.ID, link),
|
GUID: firstNonEmpty(guidFromID, srcitem.ID, link),
|
||||||
Date: dateParse(firstNonEmpty(srcitem.Published, srcitem.Updated)),
|
Date: dateParse(firstNonEmpty(srcitem.Published, srcitem.Updated)),
|
||||||
URL: link,
|
URL: link,
|
||||||
Title: srcitem.Title.Text(),
|
Title: srcitem.Title.Text(),
|
||||||
Content: firstNonEmpty(srcitem.Content.String(), srcitem.Summary.String(), srcitem.firstMediaDescription()),
|
Content: firstNonEmpty(srcitem.Content.String(), srcitem.Summary.String(), srcitem.firstMediaDescription()),
|
||||||
ImageURL: srcitem.firstMediaThumbnail(),
|
MediaLinks: mediaLinks,
|
||||||
AudioURL: "",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return dstfeed, nil
|
return dstfeed, nil
|
||||||
|
|||||||
@@ -40,13 +40,11 @@ func TestAtom(t *testing.T) {
|
|||||||
SiteURL: "http://example.org/",
|
SiteURL: "http://example.org/",
|
||||||
Items: []Item{
|
Items: []Item{
|
||||||
{
|
{
|
||||||
GUID: "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a",
|
GUID: "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a",
|
||||||
Date: time.Unix(1071340202, 0).UTC(),
|
Date: time.Unix(1071340202, 0).UTC(),
|
||||||
URL: "http://example.org/2003/12/13/atom03.html",
|
URL: "http://example.org/2003/12/13/atom03.html",
|
||||||
Title: "Atom-Powered Robots Run Amok",
|
Title: "Atom-Powered Robots Run Amok",
|
||||||
Content: `<div xmlns="http://www.w3.org/1999/xhtml"><p>This is the entry content.</p></div>`,
|
Content: `<div xmlns="http://www.w3.org/1999/xhtml"><p>This is the entry content.</p></div>`,
|
||||||
ImageURL: "",
|
|
||||||
AudioURL: "",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -94,6 +92,44 @@ func TestAtomHTMLTitle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAtomXHTMLTitle(t *testing.T) {
|
||||||
|
feed, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<entry><title type="xhtml">say <code>what</code>?</entry>
|
||||||
|
</feed>
|
||||||
|
`))
|
||||||
|
have := feed.Items[0].Title
|
||||||
|
want := "say what?"
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Logf("want: %#v", want)
|
||||||
|
t.Logf("have: %#v", have)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomXHTMLNestedTitle(t *testing.T) {
|
||||||
|
feed, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<entry>
|
||||||
|
<title type="xhtml">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<a href="https://example.com">Link to Example</a>
|
||||||
|
</div>
|
||||||
|
</title>
|
||||||
|
</entry>
|
||||||
|
</feed>
|
||||||
|
`))
|
||||||
|
have := feed.Items[0].Title
|
||||||
|
want := "Link to Example"
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Logf("want: %#v", want)
|
||||||
|
t.Logf("have: %#v", have)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAtomImageLink(t *testing.T) {
|
func TestAtomImageLink(t *testing.T) {
|
||||||
feed, _ := Parse(strings.NewReader(`
|
feed, _ := Parse(strings.NewReader(`
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -103,9 +139,15 @@ func TestAtomImageLink(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>
|
</feed>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].ImageURL
|
if len(feed.Items[0].MediaLinks) != 1 {
|
||||||
want := `https://example.com/image.png?width=100&height=100`
|
t.Fatalf("Expected 1 media link, got: %#v", feed.Items[0].MediaLinks)
|
||||||
if want != have {
|
}
|
||||||
|
have := feed.Items[0].MediaLinks[0]
|
||||||
|
want := MediaLink{
|
||||||
|
URL: `https://example.com/image.png?width=100&height=100`,
|
||||||
|
Type: "image",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
t.Fatalf("item.image_url doesn't match\nwant: %#v\nhave: %#v\n", want, have)
|
t.Fatalf("item.image_url doesn't match\nwant: %#v\nhave: %#v\n", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +169,68 @@ func TestAtomImageLinkDuplicated(t *testing.T) {
|
|||||||
if want != have {
|
if want != have {
|
||||||
t.Fatalf("want: %#v\nhave: %#v\n", want, have)
|
t.Fatalf("want: %#v\nhave: %#v\n", want, have)
|
||||||
}
|
}
|
||||||
if feed.Items[0].ImageURL != "" {
|
if len(feed.Items[0].MediaLinks) != 0 {
|
||||||
t.Fatal("item.image_url must be unset if present in the content")
|
t.Fatal("item media link must be excluded if present in the content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomLinkInID(t *testing.T) {
|
||||||
|
feed, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
|
||||||
|
<entry>
|
||||||
|
<title>one updated</title>
|
||||||
|
<id>https://example.com/posts/1</id>
|
||||||
|
<updated>2003-12-13T09:17:51</updated>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title>two</title>
|
||||||
|
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title>one</title>
|
||||||
|
<id>https://example.com/posts/1</id>
|
||||||
|
</entry>
|
||||||
|
</feed>
|
||||||
|
`))
|
||||||
|
have := feed.Items
|
||||||
|
want := []Item{
|
||||||
|
Item{
|
||||||
|
GUID: "https://example.com/posts/1::2003-12-13T09:17:51",
|
||||||
|
Date: time.Date(2003, time.December, 13, 9, 17, 51, 0, time.UTC),
|
||||||
|
URL: "https://example.com/posts/1",
|
||||||
|
Title: "one updated",
|
||||||
|
},
|
||||||
|
Item{
|
||||||
|
GUID: "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6",
|
||||||
|
Date: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), URL: "",
|
||||||
|
Title: "two",
|
||||||
|
},
|
||||||
|
Item{
|
||||||
|
GUID: "https://example.com/posts/1::",
|
||||||
|
Date: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
URL: "https://example.com/posts/1",
|
||||||
|
Title: "one",
|
||||||
|
Content: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Fatalf("\nwant: %#v\nhave: %#v\n", want, have)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomDoesntEscapeHTMLTags(t *testing.T) {
|
||||||
|
feed, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<entry><summary type="html">&lt;script&gt;alert(1);&lt;/script&gt;</summary></entry>
|
||||||
|
</feed>
|
||||||
|
`))
|
||||||
|
have := feed.Items[0].Content
|
||||||
|
want := "<script>alert(1);</script>"
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Logf("want: %#v", want)
|
||||||
|
t.Logf("have: %#v", have)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -119,6 +120,7 @@ func ParseAndFix(r io.Reader, baseURL, fallbackEncoding string) (*Feed, error) {
|
|||||||
}
|
}
|
||||||
feed.TranslateURLs(baseURL)
|
feed.TranslateURLs(baseURL)
|
||||||
feed.SetMissingDatesTo(time.Now())
|
feed.SetMissingDatesTo(time.Now())
|
||||||
|
feed.SetMissingGUIDs()
|
||||||
return feed, nil
|
return feed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +134,14 @@ func (feed *Feed) cleanup() {
|
|||||||
feed.Items[i].Title = strings.TrimSpace(htmlutil.ExtractText(item.Title))
|
feed.Items[i].Title = strings.TrimSpace(htmlutil.ExtractText(item.Title))
|
||||||
feed.Items[i].Content = strings.TrimSpace(item.Content)
|
feed.Items[i].Content = strings.TrimSpace(item.Content)
|
||||||
|
|
||||||
if item.ImageURL != "" && strings.Contains(item.Content, item.ImageURL) {
|
if len(feed.Items[i].MediaLinks) > 0 {
|
||||||
feed.Items[i].ImageURL = ""
|
mediaLinks := make([]MediaLink, 0)
|
||||||
}
|
for _, link := range item.MediaLinks {
|
||||||
if item.AudioURL != "" && strings.Contains(item.Content, item.AudioURL) {
|
if !strings.Contains(item.Content, link.URL) {
|
||||||
feed.Items[i].AudioURL = ""
|
mediaLinks = append(mediaLinks, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feed.Items[i].MediaLinks = mediaLinks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,3 +173,12 @@ func (feed *Feed) TranslateURLs(base string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (feed *Feed) SetMissingGUIDs() {
|
||||||
|
for i, item := range feed.Items {
|
||||||
|
if item.GUID == "" {
|
||||||
|
id := strings.Join([]string{item.Title, item.Date.Format(time.RFC3339), item.URL}, ";;")
|
||||||
|
feed.Items[i].GUID = fmt.Sprintf("%x", sha256.Sum256([]byte(id)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,3 +150,32 @@ func TestParseCleanIllegalCharsInNonUTF8(t *testing.T) {
|
|||||||
t.Fatalf("invalid feed, got: %v", feed)
|
t.Fatalf("invalid feed, got: %v", feed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseMissingGUID(t *testing.T) {
|
||||||
|
data := `
|
||||||
|
<?xml version="1.0" encoding="windows-1251"?>
|
||||||
|
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||||
|
<channel>
|
||||||
|
<item>
|
||||||
|
<title>foo</title>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>bar</title>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
`
|
||||||
|
feed, err := ParseAndFix(strings.NewReader(data), "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(feed.Items) != 2 {
|
||||||
|
t.Fatalf("expected 2 items, got %d", len(feed.Items))
|
||||||
|
}
|
||||||
|
if feed.Items[0].GUID == "" || feed.Items[1].GUID == "" {
|
||||||
|
t.Fatalf("item GUIDs are missing, got %#v", feed.Items)
|
||||||
|
}
|
||||||
|
if feed.Items[0].GUID == feed.Items[1].GUID {
|
||||||
|
t.Fatalf("item GUIDs are not unique, got %#v", feed.Items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type media struct {
|
type media struct {
|
||||||
MediaGroups []mediaGroup `xml:"http://search.yahoo.com/mrss/ group"`
|
MediaGroups []mediaGroup `xml:"http://search.yahoo.com/mrss/ group"`
|
||||||
MediaContents []mediaContent `xml:"http://search.yahoo.com/mrss/ content"`
|
MediaContents []mediaContent `xml:"http://search.yahoo.com/mrss/ content"`
|
||||||
@@ -8,12 +12,17 @@ type media struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mediaGroup struct {
|
type mediaGroup struct {
|
||||||
|
MediaContent []mediaContent `xml:"http://search.yahoo.com/mrss/ content"`
|
||||||
MediaThumbnails []mediaThumbnail `xml:"http://search.yahoo.com/mrss/ thumbnail"`
|
MediaThumbnails []mediaThumbnail `xml:"http://search.yahoo.com/mrss/ thumbnail"`
|
||||||
MediaDescriptions []mediaDescription `xml:"http://search.yahoo.com/mrss/ description"`
|
MediaDescriptions []mediaDescription `xml:"http://search.yahoo.com/mrss/ description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type mediaContent struct {
|
type mediaContent struct {
|
||||||
MediaThumbnails []mediaThumbnail `xml:"http://search.yahoo.com/mrss/ thumbnail"`
|
MediaThumbnails []mediaThumbnail `xml:"http://search.yahoo.com/mrss/ thumbnail"`
|
||||||
|
MediaType string `xml:"type,attr"`
|
||||||
|
MediaMedium string `xml:"medium,attr"`
|
||||||
|
MediaURL string `xml:"url,attr"`
|
||||||
|
MediaDescription mediaDescription `xml:"http://search.yahoo.com/mrss/ description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type mediaThumbnail struct {
|
type mediaThumbnail struct {
|
||||||
@@ -21,8 +30,8 @@ type mediaThumbnail struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mediaDescription struct {
|
type mediaDescription struct {
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
Description string `xml:",chardata"`
|
Text string `xml:",chardata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *media) firstMediaThumbnail() string {
|
func (m *media) firstMediaThumbnail() string {
|
||||||
@@ -44,12 +53,59 @@ func (m *media) firstMediaThumbnail() string {
|
|||||||
|
|
||||||
func (m *media) firstMediaDescription() string {
|
func (m *media) firstMediaDescription() string {
|
||||||
for _, d := range m.MediaDescriptions {
|
for _, d := range m.MediaDescriptions {
|
||||||
return plain2html(d.Description)
|
return plain2html(d.Text)
|
||||||
}
|
}
|
||||||
for _, g := range m.MediaGroups {
|
for _, g := range m.MediaGroups {
|
||||||
for _, d := range g.MediaDescriptions {
|
for _, d := range g.MediaDescriptions {
|
||||||
return plain2html(d.Description)
|
return plain2html(d.Text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *media) mediaLinks() []MediaLink {
|
||||||
|
links := make([]MediaLink, 0)
|
||||||
|
for _, thumbnail := range m.MediaThumbnails {
|
||||||
|
links = append(links, MediaLink{URL: thumbnail.URL, Type: "image"})
|
||||||
|
}
|
||||||
|
for _, group := range m.MediaGroups {
|
||||||
|
for _, thumbnail := range group.MediaThumbnails {
|
||||||
|
links = append(links, MediaLink{
|
||||||
|
URL: thumbnail.URL,
|
||||||
|
Type: "image",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, content := range m.MediaContents {
|
||||||
|
if content.MediaURL != "" {
|
||||||
|
url := content.MediaURL
|
||||||
|
description := content.MediaDescription.Text
|
||||||
|
if strings.HasPrefix(content.MediaType, "image/") {
|
||||||
|
links = append(links, MediaLink{URL: url, Type: "image", Description: description})
|
||||||
|
} else if strings.HasPrefix(content.MediaType, "audio/") {
|
||||||
|
links = append(links, MediaLink{URL: url, Type: "audio", Description: description})
|
||||||
|
} else if strings.HasPrefix(content.MediaType, "video/") {
|
||||||
|
links = append(links, MediaLink{URL: url, Type: "video", Description: description})
|
||||||
|
} else if content.MediaMedium == "image" || content.MediaMedium == "audio" || content.MediaMedium == "video" {
|
||||||
|
links = append(links, MediaLink{URL: url, Type: content.MediaMedium, Description: description})
|
||||||
|
} else {
|
||||||
|
if len(content.MediaThumbnails) > 0 {
|
||||||
|
links = append(links, MediaLink{
|
||||||
|
URL: content.MediaThumbnails[0].URL,
|
||||||
|
Type: "image",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, thumbnail := range content.MediaThumbnails {
|
||||||
|
links = append(links, MediaLink{
|
||||||
|
URL: thumbnail.URL,
|
||||||
|
Type: "image",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(links) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ type Item struct {
|
|||||||
URL string
|
URL string
|
||||||
Title string
|
Title string
|
||||||
|
|
||||||
Content string
|
Content string
|
||||||
ImageURL string
|
MediaLinks []MediaLink
|
||||||
AudioURL string
|
}
|
||||||
|
|
||||||
|
type MediaLink struct {
|
||||||
|
URL string
|
||||||
|
Type string
|
||||||
|
Description string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type rssFeed struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type rssItem struct {
|
type rssItem struct {
|
||||||
GUID rssGuid `xml:"guid"`
|
GUID rssGuid `xml:"guid"`
|
||||||
Title string `xml:"title"`
|
Title string `xml:"title"`
|
||||||
Link string `xml:"rss link"`
|
Link string `xml:"rss link"`
|
||||||
Description string `xml:"rss description"`
|
Description string `xml:"rss description"`
|
||||||
@@ -74,31 +74,30 @@ func ParseRSS(r io.Reader) (*Feed, error) {
|
|||||||
SiteURL: srcfeed.Link,
|
SiteURL: srcfeed.Link,
|
||||||
}
|
}
|
||||||
for _, srcitem := range srcfeed.Items {
|
for _, srcitem := range srcfeed.Items {
|
||||||
podcastURL := ""
|
mediaLinks := srcitem.mediaLinks()
|
||||||
for _, e := range srcitem.Enclosures {
|
for _, e := range srcitem.Enclosures {
|
||||||
if strings.HasPrefix(e.Type, "audio/") {
|
if strings.HasPrefix(e.Type, "audio/") {
|
||||||
podcastURL = e.URL
|
podcastURL := e.URL
|
||||||
|
|
||||||
if srcitem.OrigEnclosureLink != "" && strings.Contains(podcastURL, path.Base(srcitem.OrigEnclosureLink)) {
|
if srcitem.OrigEnclosureLink != "" && strings.Contains(podcastURL, path.Base(srcitem.OrigEnclosureLink)) {
|
||||||
podcastURL = srcitem.OrigEnclosureLink
|
podcastURL = srcitem.OrigEnclosureLink
|
||||||
}
|
}
|
||||||
|
mediaLinks = append(mediaLinks, MediaLink{URL: podcastURL, Type: "audio"})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
permalink := ""
|
permalink := ""
|
||||||
if srcitem.GUID.IsPermaLink == "true" {
|
if srcitem.GUID.IsPermaLink == "true" {
|
||||||
permalink = srcitem.GUID.GUID
|
permalink = srcitem.GUID.GUID
|
||||||
}
|
}
|
||||||
|
|
||||||
dstfeed.Items = append(dstfeed.Items, Item{
|
dstfeed.Items = append(dstfeed.Items, Item{
|
||||||
GUID: firstNonEmpty(srcitem.GUID.GUID, srcitem.Link),
|
GUID: firstNonEmpty(srcitem.GUID.GUID, srcitem.Link),
|
||||||
Date: dateParse(firstNonEmpty(srcitem.DublinCoreDate, srcitem.PubDate)),
|
Date: dateParse(firstNonEmpty(srcitem.DublinCoreDate, srcitem.PubDate)),
|
||||||
URL: firstNonEmpty(srcitem.OrigLink, srcitem.Link, permalink),
|
URL: firstNonEmpty(srcitem.OrigLink, srcitem.Link, permalink),
|
||||||
Title: srcitem.Title,
|
Title: srcitem.Title,
|
||||||
Content: firstNonEmpty(srcitem.ContentEncoded, srcitem.Description),
|
Content: firstNonEmpty(srcitem.ContentEncoded, srcitem.Description, srcitem.firstMediaDescription()),
|
||||||
AudioURL: podcastURL,
|
MediaLinks: mediaLinks,
|
||||||
ImageURL: srcitem.firstMediaThumbnail(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return dstfeed, nil
|
return dstfeed, nil
|
||||||
|
|||||||
@@ -75,9 +75,15 @@ func TestRSSMediaContentThumbnail(t *testing.T) {
|
|||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].ImageURL
|
if len(feed.Items[0].MediaLinks) != 1 {
|
||||||
want := "https://i.vimeocdn.com/video/1092705247_960.jpg"
|
t.Fatalf("Expected 1 media link, got %#v", feed.Items[0].MediaLinks)
|
||||||
if have != want {
|
}
|
||||||
|
have := feed.Items[0].MediaLinks[0]
|
||||||
|
want := MediaLink{
|
||||||
|
URL: "https://i.vimeocdn.com/video/1092705247_960.jpg",
|
||||||
|
Type: "image",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
t.Logf("have: %#v", have)
|
t.Logf("have: %#v", have)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -127,9 +133,15 @@ func TestRSSPodcast(t *testing.T) {
|
|||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].AudioURL
|
if len(feed.Items[0].MediaLinks) != 1 {
|
||||||
want := "http://example.com/audio.ext"
|
t.Fatal("Invalid media links")
|
||||||
if want != have {
|
}
|
||||||
|
have := feed.Items[0].MediaLinks[0]
|
||||||
|
want := MediaLink{
|
||||||
|
URL: "http://example.com/audio.ext",
|
||||||
|
Type: "audio",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
t.Logf("have: %#v", have)
|
t.Logf("have: %#v", have)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -147,9 +159,15 @@ func TestRSSOpusPodcast(t *testing.T) {
|
|||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].AudioURL
|
if len(feed.Items[0].MediaLinks) != 1 {
|
||||||
want := "http://example.com/audio.ext"
|
t.Fatal("Invalid media links")
|
||||||
if want != have {
|
}
|
||||||
|
have := feed.Items[0].MediaLinks[0]
|
||||||
|
want := MediaLink{
|
||||||
|
URL: "http://example.com/audio.ext",
|
||||||
|
Type: "audio",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
t.Logf("have: %#v", have)
|
t.Logf("have: %#v", have)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -176,8 +194,9 @@ func TestRSSPodcastDuplicated(t *testing.T) {
|
|||||||
if want != have {
|
if want != have {
|
||||||
t.Fatalf("content doesn't match\nwant: %#v\nhave: %#v\n", want, have)
|
t.Fatalf("content doesn't match\nwant: %#v\nhave: %#v\n", want, have)
|
||||||
}
|
}
|
||||||
if feed.Items[0].AudioURL != "" {
|
|
||||||
t.Fatal("item.audio_url must be unset if present in the content")
|
if len(feed.Items[0].MediaLinks) != 0 {
|
||||||
|
t.Fatal("item media must be excluded if present in the content")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,14 +236,53 @@ func TestRSSIsPermalink(t *testing.T) {
|
|||||||
`))
|
`))
|
||||||
have := feed.Items
|
have := feed.Items
|
||||||
want := []Item{
|
want := []Item{
|
||||||
{
|
{
|
||||||
GUID: "http://example.com/posts/1",
|
GUID: "http://example.com/posts/1",
|
||||||
URL: "http://example.com/posts/1",
|
URL: "http://example.com/posts/1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i := 0; i < len(want); i++ {
|
for i := 0; i < len(want); i++ {
|
||||||
if want[i] != have[i] {
|
if !reflect.DeepEqual(want, have) {
|
||||||
t.Errorf("Failed to handle isPermalink\nwant: %#v\nhave: %#v\n", want[i], have[i])
|
t.Errorf("Failed to handle isPermalink\nwant: %#v\nhave: %#v\n", want[i], have[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRSSMultipleMedia(t *testing.T) {
|
||||||
|
feed, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
|
||||||
|
<channel>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="true">http://example.com/posts/1</guid>
|
||||||
|
<media:content url="https://example.com/path/to/image1.png" type="image/png" fileSize="1000" medium="image">
|
||||||
|
<media:description type="plain">description 1</media:description>
|
||||||
|
</media:content>
|
||||||
|
<media:content url="https://example.com/path/to/image2.png" type="image/png" fileSize="2000" medium="image">
|
||||||
|
<media:description type="plain">description 2</media:description>
|
||||||
|
</media:content>
|
||||||
|
<media:content url="https://example.com/path/to/video1.mp4" type="video/mp4" fileSize="2000" medium="image">
|
||||||
|
<media:description type="plain">video description</media:description>
|
||||||
|
</media:content>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
`))
|
||||||
|
have := feed.Items
|
||||||
|
want := []Item{
|
||||||
|
{
|
||||||
|
GUID: "http://example.com/posts/1",
|
||||||
|
URL: "http://example.com/posts/1",
|
||||||
|
MediaLinks: []MediaLink{
|
||||||
|
{URL:"https://example.com/path/to/image1.png", Type:"image", Description:"description 1"},
|
||||||
|
{URL:"https://example.com/path/to/image2.png", Type:"image", Description:"description 2"},
|
||||||
|
{URL:"https://example.com/path/to/video1.mp4", Type:"video", Description:"video description"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Logf("want: %#v", want)
|
||||||
|
t.Logf("have: %#v", have)
|
||||||
|
t.Fatal("invalid rss")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !windows
|
//go:build !windows
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build macos || windows
|
//go:build (darwin || windows) && gui
|
||||||
// +build macos windows
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
@@ -11,6 +10,7 @@ import (
|
|||||||
func Start(s *server.Server) {
|
func Start(s *server.Server) {
|
||||||
systrayOnReady := func() {
|
systrayOnReady := func() {
|
||||||
systray.SetIcon(Icon)
|
systray.SetIcon(Icon)
|
||||||
|
systray.SetTooltip("yarr")
|
||||||
|
|
||||||
menuOpen := systray.AddMenuItem("Open", "")
|
menuOpen := systray.AddMenuItem("Open", "")
|
||||||
systray.AddSeparator()
|
systray.AddSeparator()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !windows && !macos
|
//go:build !gui
|
||||||
// +build !windows,!macos
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build macos
|
//go:build darwin && gui
|
||||||
// +build macos
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build windows
|
//go:build windows && gui
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !windows && !darwin
|
//go:build linux
|
||||||
// +build !windows,!darwin
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build darwin
|
//go:build darwin
|
||||||
// +build darwin
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import (
|
|||||||
|
|
||||||
"github.com/nkanaev/yarr/src/assets"
|
"github.com/nkanaev/yarr/src/assets"
|
||||||
"github.com/nkanaev/yarr/src/server/router"
|
"github.com/nkanaev/yarr/src/server/router"
|
||||||
|
"github.com/nkanaev/yarr/src/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Middleware struct {
|
type Middleware struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
BasePath string
|
BasePath string
|
||||||
Public string
|
Public []string
|
||||||
|
DB *storage.Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsafeMethod(method string) bool {
|
func unsafeMethod(method string) bool {
|
||||||
@@ -20,9 +22,11 @@ func unsafeMethod(method string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Middleware) Handler(c *router.Context) {
|
func (m *Middleware) Handler(c *router.Context) {
|
||||||
if strings.HasPrefix(c.Req.URL.Path, m.BasePath+m.Public) {
|
for _, path := range m.Public {
|
||||||
c.Next()
|
if strings.HasPrefix(c.Req.URL.Path, m.BasePath+path) {
|
||||||
return
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if IsAuthenticated(c.Req, m.Username, m.Password) {
|
if IsAuthenticated(c.Req, m.Username, m.Password) {
|
||||||
c.Next()
|
c.Next()
|
||||||
@@ -44,12 +48,15 @@ func (m *Middleware) Handler(c *router.Context) {
|
|||||||
c.Redirect(rootUrl)
|
c.Redirect(rootUrl)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
c.HTML(http.StatusOK, assets.Template("login.html"), map[string]string{
|
c.HTML(http.StatusOK, assets.Template("login.html"), map[string]interface{}{
|
||||||
"username": username,
|
"username": username,
|
||||||
"error": "Invalid username/password",
|
"error": "Invalid username/password",
|
||||||
|
"settings": m.DB.GetSettings(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, assets.Template("login.html"), nil)
|
c.HTML(http.StatusOK, assets.Template("login.html"), map[string]interface{}{
|
||||||
|
"settings": m.DB.GetSettings(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
393
src/server/fever.go
Normal file
393
src/server/fever.go
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nkanaev/yarr/src/server/auth"
|
||||||
|
"github.com/nkanaev/yarr/src/server/router"
|
||||||
|
"github.com/nkanaev/yarr/src/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeverGroup struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeverFeedsGroup struct {
|
||||||
|
GroupID int64 `json:"group_id"`
|
||||||
|
FeedIDs string `json:"feed_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeverFeed struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
FaviconID int64 `json:"favicon_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
SiteUrl string `json:"site_url"`
|
||||||
|
IsSpark int `json:"is_spark"`
|
||||||
|
LastUpdated int64 `json:"last_updated_on_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeverItem struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
FeedID int64 `json:"feed_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
HTML string `json:"html"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
IsSaved int `json:"is_saved"`
|
||||||
|
IsRead int `json:"is_read"`
|
||||||
|
CreatedAt int64 `json:"created_on_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeverFavicon struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFeverJSON(c *router.Context, data map[string]interface{}, lastRefreshed int64) {
|
||||||
|
data["api_version"] = 3
|
||||||
|
data["auth"] = 1
|
||||||
|
data["last_refreshed_on_time"] = lastRefreshed
|
||||||
|
c.JSON(http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLastRefreshedOnTime(httpStates map[int64]storage.HTTPState) int64 {
|
||||||
|
if len(httpStates) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastRefreshed int64
|
||||||
|
for _, state := range httpStates {
|
||||||
|
if state.LastRefreshed.Unix() > lastRefreshed {
|
||||||
|
lastRefreshed = state.LastRefreshed.Unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastRefreshed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverAuth(c *router.Context) bool {
|
||||||
|
if s.Username != "" && s.Password != "" {
|
||||||
|
apiKey := c.Req.FormValue("api_key")
|
||||||
|
apiKey = strings.ToLower(apiKey)
|
||||||
|
md5HashValue := md5.Sum([]byte(fmt.Sprintf("%s:%s", s.Username, s.Password)))
|
||||||
|
hexMD5HashValue := fmt.Sprintf("%x", md5HashValue[:])
|
||||||
|
if !auth.StringsEqual(apiKey, hexMD5HashValue) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func formHasValue(values url.Values, value string) bool {
|
||||||
|
if _, ok := values[value]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleFever(c *router.Context) {
|
||||||
|
c.Req.ParseForm()
|
||||||
|
if !s.feverAuth(c) {
|
||||||
|
c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"api_version": 3,
|
||||||
|
"auth": 0,
|
||||||
|
"last_refreshed_on_time": 0,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case formHasValue(c.Req.Form, "groups"):
|
||||||
|
s.feverGroupsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "feeds"):
|
||||||
|
s.feverFeedsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "unread_item_ids"):
|
||||||
|
s.feverUnreadItemIDsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "saved_item_ids"):
|
||||||
|
s.feverSavedItemIDsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "favicons"):
|
||||||
|
s.feverFaviconsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "items"):
|
||||||
|
s.feverItemsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "links"):
|
||||||
|
s.feverLinksHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "mark"):
|
||||||
|
s.feverMarkHandler(c)
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"api_version": 3,
|
||||||
|
"auth": 1,
|
||||||
|
"last_refreshed_on_time": getLastRefreshedOnTime(s.db.ListHTTPStates()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinInts(values []int64) string {
|
||||||
|
var result strings.Builder
|
||||||
|
for i, val := range values {
|
||||||
|
fmt.Fprintf(&result, "%d", val)
|
||||||
|
if i != len(values)-1 {
|
||||||
|
result.WriteString(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func feedGroups(db *storage.Storage) []*FeverFeedsGroup {
|
||||||
|
feeds := db.ListFeeds()
|
||||||
|
|
||||||
|
groupFeeds := make(map[int64][]int64)
|
||||||
|
for _, feed := range feeds {
|
||||||
|
if feed.FolderId == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
groupFeeds[*feed.FolderId] = append(groupFeeds[*feed.FolderId], feed.Id)
|
||||||
|
}
|
||||||
|
result := make([]*FeverFeedsGroup, 0)
|
||||||
|
for groupId, feedIds := range groupFeeds {
|
||||||
|
result = append(result, &FeverFeedsGroup{
|
||||||
|
GroupID: groupId,
|
||||||
|
FeedIDs: joinInts(feedIds),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverGroupsHandler(c *router.Context) {
|
||||||
|
folders := s.db.ListFolders()
|
||||||
|
groups := make([]*FeverGroup, len(folders))
|
||||||
|
for i, folder := range folders {
|
||||||
|
groups[i] = &FeverGroup{ID: folder.Id, Title: folder.Title}
|
||||||
|
}
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"groups": groups,
|
||||||
|
"feeds_groups": feedGroups(s.db),
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverFeedsHandler(c *router.Context) {
|
||||||
|
feeds := s.db.ListFeeds()
|
||||||
|
httpStates := s.db.ListHTTPStates()
|
||||||
|
|
||||||
|
feverFeeds := make([]*FeverFeed, len(feeds))
|
||||||
|
for i, feed := range feeds {
|
||||||
|
var lastUpdated int64
|
||||||
|
if state, ok := httpStates[feed.Id]; ok {
|
||||||
|
lastUpdated = state.LastRefreshed.Unix()
|
||||||
|
}
|
||||||
|
feverFeeds[i] = &FeverFeed{
|
||||||
|
ID: feed.Id,
|
||||||
|
FaviconID: feed.Id,
|
||||||
|
Title: feed.Title,
|
||||||
|
Url: feed.FeedLink,
|
||||||
|
SiteUrl: feed.Link,
|
||||||
|
IsSpark: 0,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"feeds": feverFeeds,
|
||||||
|
"feeds_groups": feedGroups(s.db),
|
||||||
|
}, getLastRefreshedOnTime(httpStates))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverFaviconsHandler(c *router.Context) {
|
||||||
|
feeds := s.db.ListFeeds()
|
||||||
|
favicons := make([]*FeverFavicon, len(feeds))
|
||||||
|
for i, feed := range feeds {
|
||||||
|
data := "data:image/gif;base64,R0lGODlhAQABAAAAACw="
|
||||||
|
if feed.HasIcon {
|
||||||
|
icon := s.db.GetFeed(feed.Id).Icon
|
||||||
|
data = fmt.Sprintf(
|
||||||
|
"data:%s;base64,%s",
|
||||||
|
http.DetectContentType(*icon),
|
||||||
|
base64.StdEncoding.EncodeToString(*icon),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
favicons[i] = &FeverFavicon{ID: feed.Id, Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"favicons": favicons,
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// for memory pressure reasons, we only return a limited number of items
|
||||||
|
// documented at https://github.com/DigitalDJ/tinytinyrss-fever-plugin/blob/master/fever-api.md#items
|
||||||
|
const listLimit = 50
|
||||||
|
|
||||||
|
func (s *Server) feverItemsHandler(c *router.Context) {
|
||||||
|
filter := storage.ItemFilter{}
|
||||||
|
query := c.Req.URL.Query()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case query.Get("with_ids") != "":
|
||||||
|
ids := make([]int64, 0)
|
||||||
|
for _, idstr := range strings.Split(query.Get("with_ids"), ",") {
|
||||||
|
if idnum, err := strconv.ParseInt(idstr, 10, 64); err == nil {
|
||||||
|
ids = append(ids, idnum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter.IDs = &ids
|
||||||
|
case query.Get("since_id") != "":
|
||||||
|
idstr := query.Get("since_id")
|
||||||
|
if idnum, err := strconv.ParseInt(idstr, 10, 64); err == nil {
|
||||||
|
filter.SinceID = &idnum
|
||||||
|
}
|
||||||
|
case query.Get("max_id") != "":
|
||||||
|
idstr := query.Get("max_id")
|
||||||
|
if idnum, err := strconv.ParseInt(idstr, 10, 64); err == nil {
|
||||||
|
filter.MaxID = &idnum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := s.db.ListItems(filter, listLimit, true, true)
|
||||||
|
|
||||||
|
feverItems := make([]FeverItem, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
date := item.Date
|
||||||
|
time := date.Unix()
|
||||||
|
|
||||||
|
isSaved := 0
|
||||||
|
if item.Status == storage.STARRED {
|
||||||
|
isSaved = 1
|
||||||
|
}
|
||||||
|
isRead := 0
|
||||||
|
if item.Status == storage.READ {
|
||||||
|
isRead = 1
|
||||||
|
}
|
||||||
|
feverItems[i] = FeverItem{
|
||||||
|
ID: item.Id,
|
||||||
|
FeedID: item.FeedId,
|
||||||
|
Title: item.Title,
|
||||||
|
Author: "",
|
||||||
|
HTML: item.Content,
|
||||||
|
Url: item.Link,
|
||||||
|
IsSaved: isSaved,
|
||||||
|
IsRead: isRead,
|
||||||
|
CreatedAt: time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalItems := s.db.CountItems(storage.ItemFilter{})
|
||||||
|
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"items": feverItems,
|
||||||
|
"total_items": totalItems,
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverLinksHandler(c *router.Context) {
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"links": make([]interface{}, 0),
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverUnreadItemIDsHandler(c *router.Context) {
|
||||||
|
status := storage.UNREAD
|
||||||
|
itemIds := make([]int64, 0)
|
||||||
|
|
||||||
|
itemFilter := storage.ItemFilter{
|
||||||
|
Status: &status,
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
items := s.db.ListItems(itemFilter, listLimit, true, false)
|
||||||
|
if len(items) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
itemIds = append(itemIds, item.Id)
|
||||||
|
}
|
||||||
|
itemFilter.After = &items[len(items)-1].Id
|
||||||
|
}
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"unread_item_ids": joinInts(itemIds),
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverSavedItemIDsHandler(c *router.Context) {
|
||||||
|
status := storage.STARRED
|
||||||
|
itemIds := make([]int64, 0)
|
||||||
|
|
||||||
|
itemFilter := storage.ItemFilter{
|
||||||
|
Status: &status,
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
items := s.db.ListItems(itemFilter, listLimit, true, false)
|
||||||
|
if len(items) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
itemIds = append(itemIds, item.Id)
|
||||||
|
}
|
||||||
|
itemFilter.After = &items[len(items)-1].Id
|
||||||
|
}
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"saved_item_ids": joinInts(itemIds),
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverMarkHandler(c *router.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Req.Form.Get("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("invalid id:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Req.Form.Get("mark") {
|
||||||
|
case "item":
|
||||||
|
var status storage.ItemStatus
|
||||||
|
switch c.Req.Form.Get("as") {
|
||||||
|
case "read":
|
||||||
|
status = storage.READ
|
||||||
|
case "unread":
|
||||||
|
status = storage.UNREAD
|
||||||
|
case "saved":
|
||||||
|
status = storage.STARRED
|
||||||
|
case "unsaved":
|
||||||
|
status = storage.READ
|
||||||
|
default:
|
||||||
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.db.UpdateItemStatus(id, status)
|
||||||
|
case "feed":
|
||||||
|
if c.Req.Form.Get("as") != "read" {
|
||||||
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
markFilter := storage.MarkFilter{FeedID: &id}
|
||||||
|
x, _ := strconv.ParseInt(c.Req.Form.Get("before"), 10, 64)
|
||||||
|
if x > 0 {
|
||||||
|
before := time.Unix(x, 0)
|
||||||
|
markFilter.Before = &before
|
||||||
|
}
|
||||||
|
s.db.MarkItemsRead(markFilter)
|
||||||
|
case "group":
|
||||||
|
if c.Req.Form.Get("as") != "read" {
|
||||||
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
markFilter := storage.MarkFilter{FolderID: &id}
|
||||||
|
x, _ := strconv.ParseInt(c.Req.Form.Get("before"), 10, 64)
|
||||||
|
if x > 0 {
|
||||||
|
before := time.Unix(x, 0)
|
||||||
|
markFilter.Before = &before
|
||||||
|
}
|
||||||
|
s.db.MarkItemsRead(markFilter)
|
||||||
|
default:
|
||||||
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"api_version": 3,
|
||||||
|
"auth": 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nkanaev/yarr/src/assets"
|
"github.com/nkanaev/yarr/src/assets"
|
||||||
|
"github.com/nkanaev/yarr/src/content/htmlutil"
|
||||||
"github.com/nkanaev/yarr/src/content/readability"
|
"github.com/nkanaev/yarr/src/content/readability"
|
||||||
"github.com/nkanaev/yarr/src/content/sanitizer"
|
"github.com/nkanaev/yarr/src/content/sanitizer"
|
||||||
"github.com/nkanaev/yarr/src/content/silo"
|
"github.com/nkanaev/yarr/src/content/silo"
|
||||||
@@ -33,7 +34,8 @@ func (s *Server) handler() http.Handler {
|
|||||||
BasePath: s.BasePath,
|
BasePath: s.BasePath,
|
||||||
Username: s.Username,
|
Username: s.Username,
|
||||||
Password: s.Password,
|
Password: s.Password,
|
||||||
Public: "/static",
|
Public: []string{"/static", "/fever"},
|
||||||
|
DB: s.db,
|
||||||
}
|
}
|
||||||
r.Use(a.Handler)
|
r.Use(a.Handler)
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,7 @@ func (s *Server) handler() http.Handler {
|
|||||||
r.For("/opml/export", s.handleOPMLExport)
|
r.For("/opml/export", s.handleOPMLExport)
|
||||||
r.For("/page", s.handlePageCrawl)
|
r.For("/page", s.handlePageCrawl)
|
||||||
r.For("/logout", s.handleLogout)
|
r.For("/logout", s.handleLogout)
|
||||||
|
r.For("/fever/", s.handleFever)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -84,7 +87,7 @@ func (s *Server) handleManifest(c *router.Context) {
|
|||||||
"short_name": "yarr",
|
"short_name": "yarr",
|
||||||
"description": "yet another rss reader",
|
"description": "yet another rss reader",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"start_url": s.BasePath,
|
"start_url": "/" + strings.TrimPrefix(s.BasePath, "/"),
|
||||||
"icons": []map[string]interface{}{
|
"icons": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"src": s.BasePath + "/static/graphicarts/favicon.png",
|
"src": s.BasePath + "/static/graphicarts/favicon.png",
|
||||||
@@ -291,6 +294,11 @@ func (s *Server) handleFeed(c *router.Context) {
|
|||||||
s.db.UpdateFeedFolder(id, &folderId)
|
s.db.UpdateFeedFolder(id, &folderId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if link, ok := body["feed_link"]; ok {
|
||||||
|
if reflect.TypeOf(link).Kind() == reflect.String {
|
||||||
|
s.db.UpdateFeedLink(id, link.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
c.Out.WriteHeader(http.StatusOK)
|
c.Out.WriteHeader(http.StatusOK)
|
||||||
} else if c.Req.Method == "DELETE" {
|
} else if c.Req.Method == "DELETE" {
|
||||||
s.db.DeleteFeed(id)
|
s.db.DeleteFeed(id)
|
||||||
@@ -312,7 +320,18 @@ func (s *Server) handleItem(c *router.Context) {
|
|||||||
c.Out.WriteHeader(http.StatusBadRequest)
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runtime fix for relative links
|
||||||
|
if !htmlutil.IsAPossibleLink(item.Link) {
|
||||||
|
if feed := s.db.GetFeed(item.FeedId); feed != nil {
|
||||||
|
item.Link = htmlutil.AbsoluteUrl(item.Link, feed.Link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
item.Content = sanitizer.Sanitize(item.Link, item.Content)
|
item.Content = sanitizer.Sanitize(item.Link, item.Content)
|
||||||
|
for i, link := range item.MediaLinks {
|
||||||
|
item.MediaLinks[i].Description = sanitizer.Sanitize(item.Link, link.Description)
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, item)
|
c.JSON(http.StatusOK, item)
|
||||||
} else if c.Req.Method == "PUT" {
|
} else if c.Req.Method == "PUT" {
|
||||||
@@ -355,12 +374,19 @@ func (s *Server) handleItemList(c *router.Context) {
|
|||||||
}
|
}
|
||||||
newestFirst := query.Get("oldest_first") != "true"
|
newestFirst := query.Get("oldest_first") != "true"
|
||||||
|
|
||||||
items := s.db.ListItems(filter, perPage+1, newestFirst)
|
items := s.db.ListItems(filter, perPage+1, newestFirst, true)
|
||||||
hasMore := false
|
hasMore := false
|
||||||
if len(items) == perPage+1 {
|
if len(items) == perPage+1 {
|
||||||
hasMore = true
|
hasMore = true
|
||||||
items = items[:perPage]
|
items = items[:perPage]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i, item := range items {
|
||||||
|
if item.Title == "" {
|
||||||
|
text := htmlutil.ExtractText(item.Content)
|
||||||
|
items[i].Title = htmlutil.TruncateText(text, 140)
|
||||||
|
}
|
||||||
|
}
|
||||||
c.JSON(http.StatusOK, map[string]interface{}{
|
c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
"list": items,
|
"list": items,
|
||||||
"has_more": hasMore,
|
"has_more": hasMore,
|
||||||
|
|||||||
@@ -20,18 +20,19 @@ func (s *Storage) CreateFeed(title, description, link, feedLink string, folderId
|
|||||||
if title == "" {
|
if title == "" {
|
||||||
title = feedLink
|
title = feedLink
|
||||||
}
|
}
|
||||||
result, err := s.db.Exec(`
|
row := s.db.QueryRow(`
|
||||||
insert into feeds (title, description, link, feed_link, folder_id)
|
insert into feeds (title, description, link, feed_link, folder_id)
|
||||||
values (?, ?, ?, ?, ?)
|
values (?, ?, ?, ?, ?)
|
||||||
on conflict (feed_link) do update set folder_id=?`,
|
on conflict (feed_link) do update set folder_id = ?
|
||||||
|
returning id`,
|
||||||
title, description, link, feedLink, folderId,
|
title, description, link, feedLink, folderId,
|
||||||
folderId,
|
folderId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
log.Print(err)
|
||||||
}
|
|
||||||
id, idErr := result.LastInsertId()
|
|
||||||
if idErr != nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &Feed{
|
return &Feed{
|
||||||
@@ -70,6 +71,11 @@ func (s *Storage) UpdateFeedFolder(feedId int64, newFolderId *int64) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Storage) UpdateFeedLink(feedId int64, newLink string) bool {
|
||||||
|
_, err := s.db.Exec(`update feeds set feed_link = ? where id = ?`, newLink, feedId)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Storage) UpdateFeedIcon(feedId int64, icon *[]byte) bool {
|
func (s *Storage) UpdateFeedIcon(feedId int64, icon *[]byte) bool {
|
||||||
_, err := s.db.Exec(`update feeds set icon = ? where id = ?`, icon, feedId)
|
_, err := s.db.Exec(`update feeds set icon = ? where id = ?`, icon, feedId)
|
||||||
return err == nil
|
return err == nil
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ func TestCreateFeed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateFeedSameLink(t *testing.T) {
|
||||||
|
db := testDB()
|
||||||
|
feed1 := db.CreateFeed("title", "", "", "http://example1.com/feed.xml", nil)
|
||||||
|
if feed1 == nil || feed1.Id == 0 {
|
||||||
|
t.Fatal("expected feed")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
db.CreateFeed("title", "", "", "http://example2.com/feed.xml", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
feed2 := db.CreateFeed("title", "", "http://example.com", "http://example1.com/feed.xml", nil)
|
||||||
|
if feed1.Id != feed2.Id {
|
||||||
|
t.Fatalf("expected the same feed.\nwant: %#v\nhave: %#v", feed1, feed2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReadFeed(t *testing.T) {
|
func TestReadFeed(t *testing.T) {
|
||||||
db := testDB()
|
db := testDB()
|
||||||
if db.GetFeed(100500) != nil {
|
if db.GetFeed(100500) != nil {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,35 +12,21 @@ type Folder struct {
|
|||||||
|
|
||||||
func (s *Storage) CreateFolder(title string) *Folder {
|
func (s *Storage) CreateFolder(title string) *Folder {
|
||||||
expanded := true
|
expanded := true
|
||||||
result, err := s.db.Exec(`
|
row := s.db.QueryRow(`
|
||||||
insert into folders (title, is_expanded) values (?, ?)
|
insert into folders (title, is_expanded) values (?, ?)
|
||||||
on conflict (title) do nothing`,
|
on conflict (title) do update set title = ?
|
||||||
|
returning id`,
|
||||||
title, expanded,
|
title, expanded,
|
||||||
|
// provide title again so that we can extract row id
|
||||||
|
title,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var id int64
|
var id int64
|
||||||
numrows, err := result.RowsAffected()
|
err := row.Scan(&id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if numrows == 1 {
|
|
||||||
id, err = result.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = s.db.QueryRow(`select id, is_expanded from folders where title=?`, title).Scan(&id, &expanded)
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &Folder{Id: id, Title: title, IsExpanded: expanded}
|
return &Folder{Id: id, Title: title, IsExpanded: expanded}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -43,17 +45,35 @@ func (s *ItemStatus) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MediaLink struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaLinks []MediaLink
|
||||||
|
|
||||||
|
func (m *MediaLinks) Scan(src any) error {
|
||||||
|
if data, ok := src.([]byte); ok {
|
||||||
|
return json.Unmarshal(data, m)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MediaLinks) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
GUID string `json:"guid"`
|
GUID string `json:"guid"`
|
||||||
FeedId int64 `json:"feed_id"`
|
FeedId int64 `json:"feed_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Link string `json:"link"`
|
Link string `json:"link"`
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Status ItemStatus `json:"status"`
|
Status ItemStatus `json:"status"`
|
||||||
ImageURL *string `json:"image"`
|
MediaLinks MediaLinks `json:"media_links"`
|
||||||
AudioURL *string `json:"podcast_url"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ItemFilter struct {
|
type ItemFilter struct {
|
||||||
@@ -62,11 +82,35 @@ type ItemFilter struct {
|
|||||||
Status *ItemStatus
|
Status *ItemStatus
|
||||||
Search *string
|
Search *string
|
||||||
After *int64
|
After *int64
|
||||||
|
IDs *[]int64
|
||||||
|
SinceID *int64
|
||||||
|
MaxID *int64
|
||||||
|
Before *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarkFilter struct {
|
type MarkFilter struct {
|
||||||
FolderID *int64
|
FolderID *int64
|
||||||
FeedID *int64
|
FeedID *int64
|
||||||
|
|
||||||
|
Before *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemList []Item
|
||||||
|
|
||||||
|
func (list ItemList) Len() int {
|
||||||
|
return len(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list ItemList) SortKey(i int) string {
|
||||||
|
return list[i].Date.Format(time.RFC3339) + "::" + list[i].GUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list ItemList) Less(i, j int) bool {
|
||||||
|
return list.SortKey(i) < list.SortKey(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list ItemList) Swap(i, j int) {
|
||||||
|
list[i], list[j] = list[j], list[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) CreateItems(items []Item) bool {
|
func (s *Storage) CreateItems(items []Item) bool {
|
||||||
@@ -78,17 +122,24 @@ func (s *Storage) CreateItems(items []Item) bool {
|
|||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
for _, item := range items {
|
itemsSorted := ItemList(items)
|
||||||
|
sort.Sort(itemsSorted)
|
||||||
|
|
||||||
|
for _, item := range itemsSorted {
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
insert into items (
|
insert into items (
|
||||||
guid, feed_id, title, link, date,
|
guid, feed_id, title, link, date,
|
||||||
content, image, podcast_url,
|
content, media_links,
|
||||||
date_arrived, status
|
date_arrived, status
|
||||||
)
|
)
|
||||||
values (?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%f', ?), ?, ?, ?, ?, ?)
|
values (
|
||||||
|
?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%f', ?),
|
||||||
|
?, ?,
|
||||||
|
?, ?
|
||||||
|
)
|
||||||
on conflict (feed_id, guid) do nothing`,
|
on conflict (feed_id, guid) do nothing`,
|
||||||
item.GUID, item.FeedId, item.Title, item.Link, item.Date,
|
item.GUID, item.FeedId, item.Title, item.Link, item.Date,
|
||||||
item.Content, item.ImageURL, item.AudioURL,
|
item.Content, item.MediaLinks,
|
||||||
now, UNREAD,
|
now, UNREAD,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -140,6 +191,28 @@ func listQueryPredicate(filter ItemFilter, newestFirst bool) (string, []interfac
|
|||||||
cond = append(cond, fmt.Sprintf("(i.date, i.id) %s (select date, id from items where id = ?)", compare))
|
cond = append(cond, fmt.Sprintf("(i.date, i.id) %s (select date, id from items where id = ?)", compare))
|
||||||
args = append(args, *filter.After)
|
args = append(args, *filter.After)
|
||||||
}
|
}
|
||||||
|
if filter.IDs != nil && len(*filter.IDs) > 0 {
|
||||||
|
qmarks := make([]string, len(*filter.IDs))
|
||||||
|
idargs := make([]interface{}, len(*filter.IDs))
|
||||||
|
for i, id := range *filter.IDs {
|
||||||
|
qmarks[i] = "?"
|
||||||
|
idargs[i] = id
|
||||||
|
}
|
||||||
|
cond = append(cond, "i.id in ("+strings.Join(qmarks, ",")+")")
|
||||||
|
args = append(args, idargs...)
|
||||||
|
}
|
||||||
|
if filter.SinceID != nil {
|
||||||
|
cond = append(cond, "i.id > ?")
|
||||||
|
args = append(args, filter.SinceID)
|
||||||
|
}
|
||||||
|
if filter.MaxID != nil {
|
||||||
|
cond = append(cond, "i.id < ?")
|
||||||
|
args = append(args, filter.MaxID)
|
||||||
|
}
|
||||||
|
if filter.Before != nil {
|
||||||
|
cond = append(cond, "i.date < ?")
|
||||||
|
args = append(args, filter.Before)
|
||||||
|
}
|
||||||
|
|
||||||
predicate := "1"
|
predicate := "1"
|
||||||
if len(cond) > 0 {
|
if len(cond) > 0 {
|
||||||
@@ -149,7 +222,24 @@ func listQueryPredicate(filter ItemFilter, newestFirst bool) (string, []interfac
|
|||||||
return predicate, args
|
return predicate, args
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool) []Item {
|
func (s *Storage) CountItems(filter ItemFilter) int {
|
||||||
|
predicate, args := listQueryPredicate(filter, false)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
select count(*)
|
||||||
|
from items
|
||||||
|
where %s
|
||||||
|
`, predicate)
|
||||||
|
err := s.db.QueryRow(query, args...).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool, withContent bool) []Item {
|
||||||
predicate, args := listQueryPredicate(filter, newestFirst)
|
predicate, args := listQueryPredicate(filter, newestFirst)
|
||||||
result := make([]Item, 0, 0)
|
result := make([]Item, 0, 0)
|
||||||
|
|
||||||
@@ -157,17 +247,26 @@ func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool) []It
|
|||||||
if !newestFirst {
|
if !newestFirst {
|
||||||
order = "date asc, id asc"
|
order = "date asc, id asc"
|
||||||
}
|
}
|
||||||
|
if filter.IDs != nil || filter.SinceID != nil {
|
||||||
|
order = "i.id asc"
|
||||||
|
}
|
||||||
|
if filter.MaxID != nil {
|
||||||
|
order = "i.id desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCols := "i.id, i.guid, i.feed_id, i.title, i.link, i.date, i.status, i.media_links"
|
||||||
|
if withContent {
|
||||||
|
selectCols += ", i.content"
|
||||||
|
} else {
|
||||||
|
selectCols += ", '' as content"
|
||||||
|
}
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
select
|
select %s
|
||||||
i.id, i.guid, i.feed_id,
|
|
||||||
i.title, i.link, i.date,
|
|
||||||
i.status, i.image, i.podcast_url
|
|
||||||
from items i
|
from items i
|
||||||
where %s
|
where %s
|
||||||
order by %s
|
order by %s
|
||||||
limit %d
|
limit %d
|
||||||
`, predicate, order, limit)
|
`, selectCols, predicate, order, limit)
|
||||||
rows, err := s.db.Query(query, args...)
|
rows, err := s.db.Query(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@@ -178,7 +277,7 @@ func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool) []It
|
|||||||
err = rows.Scan(
|
err = rows.Scan(
|
||||||
&x.Id, &x.GUID, &x.FeedId,
|
&x.Id, &x.GUID, &x.FeedId,
|
||||||
&x.Title, &x.Link, &x.Date,
|
&x.Title, &x.Link, &x.Date,
|
||||||
&x.Status, &x.ImageURL, &x.AudioURL,
|
&x.Status, &x.MediaLinks, &x.Content,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@@ -194,12 +293,12 @@ func (s *Storage) GetItem(id int64) *Item {
|
|||||||
err := s.db.QueryRow(`
|
err := s.db.QueryRow(`
|
||||||
select
|
select
|
||||||
i.id, i.guid, i.feed_id, i.title, i.link, i.content,
|
i.id, i.guid, i.feed_id, i.title, i.link, i.content,
|
||||||
i.date, i.status, i.image, i.podcast_url
|
i.date, i.status, i.media_links
|
||||||
from items i
|
from items i
|
||||||
where i.id = ?
|
where i.id = ?
|
||||||
`, id).Scan(
|
`, id).Scan(
|
||||||
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
|
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
|
||||||
&i.Date, &i.Status, &i.ImageURL, &i.AudioURL,
|
&i.Date, &i.Status, &i.MediaLinks,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@@ -214,7 +313,11 @@ func (s *Storage) UpdateItemStatus(item_id int64, status ItemStatus) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) MarkItemsRead(filter MarkFilter) bool {
|
func (s *Storage) MarkItemsRead(filter MarkFilter) bool {
|
||||||
predicate, args := listQueryPredicate(ItemFilter{FolderID: filter.FolderID, FeedID: filter.FeedID}, false)
|
predicate, args := listQueryPredicate(ItemFilter{
|
||||||
|
FolderID: filter.FolderID,
|
||||||
|
FeedID: filter.FeedID,
|
||||||
|
Before: filter.Before,
|
||||||
|
}, false)
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
update items as i set status = %d
|
update items as i set status = %d
|
||||||
where %s and i.status != %d
|
where %s and i.status != %d
|
||||||
|
|||||||
@@ -77,12 +77,12 @@ func getItem(db *Storage, guid string) *Item {
|
|||||||
err := db.db.QueryRow(`
|
err := db.db.QueryRow(`
|
||||||
select
|
select
|
||||||
i.id, i.guid, i.feed_id, i.title, i.link, i.content,
|
i.id, i.guid, i.feed_id, i.title, i.link, i.content,
|
||||||
i.date, i.status, i.image, i.podcast_url
|
i.date, i.status, i.media_links
|
||||||
from items i
|
from items i
|
||||||
where i.guid = ?
|
where i.guid = ?
|
||||||
`, guid).Scan(
|
`, guid).Scan(
|
||||||
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
|
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
|
||||||
&i.Date, &i.Status, &i.ImageURL, &i.AudioURL,
|
&i.Date, &i.Status, &i.MediaLinks,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -104,7 +104,7 @@ func TestListItems(t *testing.T) {
|
|||||||
|
|
||||||
// filter by folder_id
|
// filter by folder_id
|
||||||
|
|
||||||
have := getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder1.Id}, 10, false))
|
have := getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder1.Id}, 10, false, false))
|
||||||
want := []string{"item111", "item112", "item113", "item121", "item122"}
|
want := []string{"item111", "item112", "item113", "item121", "item122"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -112,7 +112,7 @@ func TestListItems(t *testing.T) {
|
|||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder2.Id}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder2.Id}, 10, false, false))
|
||||||
want = []string{"item211", "item212"}
|
want = []string{"item211", "item212"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -122,7 +122,7 @@ func TestListItems(t *testing.T) {
|
|||||||
|
|
||||||
// filter by feed_id
|
// filter by feed_id
|
||||||
|
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed11.Id}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed11.Id}, 10, false, false))
|
||||||
want = []string{"item111", "item112", "item113"}
|
want = []string{"item111", "item112", "item113"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -130,7 +130,7 @@ func TestListItems(t *testing.T) {
|
|||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed01.Id}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed01.Id}, 10, false, false))
|
||||||
want = []string{"item011", "item012", "item013"}
|
want = []string{"item011", "item012", "item013"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -141,7 +141,7 @@ func TestListItems(t *testing.T) {
|
|||||||
// filter by status
|
// filter by status
|
||||||
|
|
||||||
var starred ItemStatus = STARRED
|
var starred ItemStatus = STARRED
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{Status: &starred}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{Status: &starred}, 10, false, false))
|
||||||
want = []string{"item113", "item212", "item013"}
|
want = []string{"item113", "item212", "item013"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -150,7 +150,7 @@ func TestListItems(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var unread ItemStatus = UNREAD
|
var unread ItemStatus = UNREAD
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{Status: &unread}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{Status: &unread}, 10, false, false))
|
||||||
want = []string{"item111", "item121", "item011"}
|
want = []string{"item111", "item121", "item011"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -160,7 +160,7 @@ func TestListItems(t *testing.T) {
|
|||||||
|
|
||||||
// limit
|
// limit
|
||||||
|
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{}, 2, false))
|
have = getItemGuids(db.ListItems(ItemFilter{}, 2, false, false))
|
||||||
want = []string{"item111", "item112"}
|
want = []string{"item111", "item112"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -171,7 +171,7 @@ func TestListItems(t *testing.T) {
|
|||||||
// filter by search
|
// filter by search
|
||||||
db.SyncSearch()
|
db.SyncSearch()
|
||||||
search1 := "title111"
|
search1 := "title111"
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{Search: &search1}, 4, true))
|
have = getItemGuids(db.ListItems(ItemFilter{Search: &search1}, 4, true, false))
|
||||||
want = []string{"item111"}
|
want = []string{"item111"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -180,7 +180,7 @@ func TestListItems(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sort by date
|
// sort by date
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{}, 4, true))
|
have = getItemGuids(db.ListItems(ItemFilter{}, 4, true, false))
|
||||||
want = []string{"item013", "item012", "item011", "item212"}
|
want = []string{"item013", "item012", "item011", "item212"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -197,7 +197,7 @@ func TestListItemsPaginated(t *testing.T) {
|
|||||||
item121 := getItem(db, "item121")
|
item121 := getItem(db, "item121")
|
||||||
|
|
||||||
// all, newest first
|
// all, newest first
|
||||||
have := getItemGuids(db.ListItems(ItemFilter{After: &item012.Id}, 3, true))
|
have := getItemGuids(db.ListItems(ItemFilter{After: &item012.Id}, 3, true, false))
|
||||||
want := []string{"item011", "item212", "item211"}
|
want := []string{"item011", "item212", "item211"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -207,7 +207,7 @@ func TestListItemsPaginated(t *testing.T) {
|
|||||||
|
|
||||||
// unread, newest first
|
// unread, newest first
|
||||||
unread := UNREAD
|
unread := UNREAD
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{After: &item012.Id, Status: &unread}, 3, true))
|
have = getItemGuids(db.ListItems(ItemFilter{After: &item012.Id, Status: &unread}, 3, true, false))
|
||||||
want = []string{"item011", "item121", "item111"}
|
want = []string{"item011", "item121", "item111"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -217,7 +217,7 @@ func TestListItemsPaginated(t *testing.T) {
|
|||||||
|
|
||||||
// starred, oldest first
|
// starred, oldest first
|
||||||
starred := STARRED
|
starred := STARRED
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{After: &item121.Id, Status: &starred}, 3, false))
|
have = getItemGuids(db.ListItems(ItemFilter{After: &item121.Id, Status: &starred}, 3, false, false))
|
||||||
want = []string{"item212", "item013"}
|
want = []string{"item212", "item013"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -233,7 +233,7 @@ func TestMarkItemsRead(t *testing.T) {
|
|||||||
db1 := testDB()
|
db1 := testDB()
|
||||||
testItemsSetup(db1)
|
testItemsSetup(db1)
|
||||||
db1.MarkItemsRead(MarkFilter{})
|
db1.MarkItemsRead(MarkFilter{})
|
||||||
have := getItemGuids(db1.ListItems(ItemFilter{Status: &read}, 10, false))
|
have := getItemGuids(db1.ListItems(ItemFilter{Status: &read}, 10, false, false))
|
||||||
want := []string{
|
want := []string{
|
||||||
"item111", "item112", "item121", "item122",
|
"item111", "item112", "item121", "item122",
|
||||||
"item211", "item011", "item012",
|
"item211", "item011", "item012",
|
||||||
@@ -247,7 +247,7 @@ func TestMarkItemsRead(t *testing.T) {
|
|||||||
db2 := testDB()
|
db2 := testDB()
|
||||||
scope2 := testItemsSetup(db2)
|
scope2 := testItemsSetup(db2)
|
||||||
db2.MarkItemsRead(MarkFilter{FolderID: &scope2.folder1.Id})
|
db2.MarkItemsRead(MarkFilter{FolderID: &scope2.folder1.Id})
|
||||||
have = getItemGuids(db2.ListItems(ItemFilter{Status: &read}, 10, false))
|
have = getItemGuids(db2.ListItems(ItemFilter{Status: &read}, 10, false, false))
|
||||||
want = []string{
|
want = []string{
|
||||||
"item111", "item112", "item121", "item122",
|
"item111", "item112", "item121", "item122",
|
||||||
"item211", "item012",
|
"item211", "item012",
|
||||||
@@ -261,7 +261,7 @@ func TestMarkItemsRead(t *testing.T) {
|
|||||||
db3 := testDB()
|
db3 := testDB()
|
||||||
scope3 := testItemsSetup(db3)
|
scope3 := testItemsSetup(db3)
|
||||||
db3.MarkItemsRead(MarkFilter{FeedID: &scope3.feed11.Id})
|
db3.MarkItemsRead(MarkFilter{FeedID: &scope3.feed11.Id})
|
||||||
have = getItemGuids(db3.ListItems(ItemFilter{Status: &read}, 10, false))
|
have = getItemGuids(db3.ListItems(ItemFilter{Status: &read}, 10, false, false))
|
||||||
want = []string{
|
want = []string{
|
||||||
"item111", "item112", "item122",
|
"item111", "item112", "item122",
|
||||||
"item211", "item012",
|
"item211", "item012",
|
||||||
@@ -319,7 +319,7 @@ func TestDeleteOldItems(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
db.DeleteOldItems()
|
db.DeleteOldItems()
|
||||||
feedItems := db.ListItems(ItemFilter{FeedID: &feed.Id}, 1000, false)
|
feedItems := db.ListItems(ItemFilter{FeedID: &feed.Id}, 1000, false, false)
|
||||||
if len(feedItems) != len(items)-3 {
|
if len(feedItems) != len(items)-3 {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"invalid number of old items kept\nwant: %d\nhave: %d",
|
"invalid number of old items kept\nwant: %d\nhave: %d",
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ var migrations = []func(*sql.Tx) error{
|
|||||||
m06_fill_missing_dates,
|
m06_fill_missing_dates,
|
||||||
m07_add_feed_size,
|
m07_add_feed_size,
|
||||||
m08_normalize_datetime,
|
m08_normalize_datetime,
|
||||||
|
m09_change_item_index,
|
||||||
|
m10_add_item_medialinks,
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxVersion = int64(len(migrations))
|
var maxVersion = int64(len(migrations))
|
||||||
|
|
||||||
func migrate(db *sql.DB) error {
|
func migrate(db *sql.DB) error {
|
||||||
var version int64
|
var version int64
|
||||||
db.QueryRow("pragma user_version").Scan(&version)
|
if err := db.QueryRow("pragma user_version").Scan(&version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if version >= maxVersion {
|
if version >= maxVersion {
|
||||||
return nil
|
return nil
|
||||||
@@ -294,3 +298,37 @@ func m08_normalize_datetime(tx *sql.Tx) error {
|
|||||||
_, err = tx.Exec(`update items set date = strftime('%Y-%m-%d %H:%M:%f', date);`)
|
_, err = tx.Exec(`update items set date = strftime('%Y-%m-%d %H:%M:%f', date);`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func m09_change_item_index(tx *sql.Tx) error {
|
||||||
|
sql := `
|
||||||
|
drop index if exists idx_item_status;
|
||||||
|
create index if not exists idx_item__date_id_status on items(date,id,status);
|
||||||
|
`
|
||||||
|
_, err := tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func m10_add_item_medialinks(tx *sql.Tx) error {
|
||||||
|
sql := `
|
||||||
|
alter table items add column media_links blob;
|
||||||
|
update items set media_links =
|
||||||
|
iif(
|
||||||
|
coalesce(image, '') != '' and coalesce(podcast_url, '') != '',
|
||||||
|
json_array(json_object('type', 'image', 'url', image), json_object('type', 'audio', 'url', podcast_url)),
|
||||||
|
iif(
|
||||||
|
coalesce(image, '') != '',
|
||||||
|
json_array(json_object('type', 'image', 'url', image)),
|
||||||
|
iif(
|
||||||
|
coalesce(podcast_url, '') != '',
|
||||||
|
json_array(json_object('type', 'audio', 'url', podcast_url)),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table items drop column image;
|
||||||
|
alter table items drop column podcast_url;
|
||||||
|
`
|
||||||
|
_, err := tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,14 +13,17 @@ type Storage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(path string) (*Storage, error) {
|
func New(path string) (*Storage, error) {
|
||||||
|
if pos := strings.IndexRune(path, '?'); pos == -1 {
|
||||||
|
params := "_journal=WAL&_sync=NORMAL&_busy_timeout=5000&cache=shared"
|
||||||
|
log.Printf("opening db with params: %s", params)
|
||||||
|
path = path + "?" + params
|
||||||
|
}
|
||||||
|
|
||||||
db, err := sql.Open("sqlite3", path)
|
db, err := sql.Open("sqlite3", path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: https://foxcpp.dev/articles/the-right-way-to-use-go-sqlite3
|
|
||||||
db.SetMaxOpenConns(1)
|
|
||||||
|
|
||||||
if err = migrate(db); err != nil {
|
if err = migrate(db); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build darwin || windows
|
||||||
// +build darwin windows
|
// +build darwin windows
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build never
|
||||||
// +build never
|
// +build never
|
||||||
|
|
||||||
package systray
|
package systray
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build darwin
|
||||||
// +build darwin
|
// +build darwin
|
||||||
|
|
||||||
package systray
|
package systray
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package systray
|
package systray
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package systray
|
package systray
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ func (c *Client) getConditional(url, lastModified, etag string) (*http.Response,
|
|||||||
|
|
||||||
var client *Client
|
var client *Client
|
||||||
|
|
||||||
|
func SetVersion(num string) {
|
||||||
|
client.userAgent = "Yarr/" + num
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
|||||||
@@ -143,24 +143,19 @@ func ConvertItems(items []parser.Item, feed storage.Feed) []storage.Item {
|
|||||||
result := make([]storage.Item, len(items))
|
result := make([]storage.Item, len(items))
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
item := item
|
item := item
|
||||||
var audioURL *string = nil
|
mediaLinks := make(storage.MediaLinks, 0)
|
||||||
if item.AudioURL != "" {
|
for _, link := range item.MediaLinks {
|
||||||
audioURL = &item.AudioURL
|
mediaLinks = append(mediaLinks, storage.MediaLink(link))
|
||||||
}
|
|
||||||
var imageURL *string = nil
|
|
||||||
if item.ImageURL != "" {
|
|
||||||
imageURL = &item.ImageURL
|
|
||||||
}
|
}
|
||||||
result[i] = storage.Item{
|
result[i] = storage.Item{
|
||||||
GUID: item.GUID,
|
GUID: item.GUID,
|
||||||
FeedId: feed.Id,
|
FeedId: feed.Id,
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Link: item.URL,
|
Link: item.URL,
|
||||||
Content: item.Content,
|
Content: item.Content,
|
||||||
Date: item.Date,
|
Date: item.Date,
|
||||||
Status: storage.UNREAD,
|
Status: storage.UNREAD,
|
||||||
ImageURL: imageURL,
|
MediaLinks: mediaLinks,
|
||||||
AudioURL: audioURL,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
4
vendor/golang.org/x/net/LICENSE
generated
vendored
4
vendor/golang.org/x/net/LICENSE
generated
vendored
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
Copyright 2009 The Go Authors.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are
|
||||||
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
|
|||||||
copyright notice, this list of conditions and the following disclaimer
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
in the documentation and/or other materials provided with the
|
in the documentation and/or other materials provided with the
|
||||||
distribution.
|
distribution.
|
||||||
* Neither the name of Google Inc. nor the names of its
|
* Neither the name of Google LLC nor the names of its
|
||||||
contributors may be used to endorse or promote products derived from
|
contributors may be used to endorse or promote products derived from
|
||||||
this software without specific prior written permission.
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
|||||||
27
vendor/golang.org/x/net/html/doc.go
generated
vendored
27
vendor/golang.org/x/net/html/doc.go
generated
vendored
@@ -78,16 +78,11 @@ example, to process each anchor node in depth-first order:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
var f func(*html.Node)
|
for n := range doc.Descendants() {
|
||||||
f = func(n *html.Node) {
|
|
||||||
if n.Type == html.ElementNode && n.Data == "a" {
|
if n.Type == html.ElementNode && n.Data == "a" {
|
||||||
// Do something with n...
|
// Do something with n...
|
||||||
}
|
}
|
||||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
||||||
f(c)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
f(doc)
|
|
||||||
|
|
||||||
The relevant specifications include:
|
The relevant specifications include:
|
||||||
https://html.spec.whatwg.org/multipage/syntax.html and
|
https://html.spec.whatwg.org/multipage/syntax.html and
|
||||||
@@ -99,14 +94,20 @@ Care should be taken when parsing and interpreting HTML, whether full documents
|
|||||||
or fragments, within the framework of the HTML specification, especially with
|
or fragments, within the framework of the HTML specification, especially with
|
||||||
regard to untrusted inputs.
|
regard to untrusted inputs.
|
||||||
|
|
||||||
This package provides both a tokenizer and a parser. Only the parser constructs
|
This package provides both a tokenizer and a parser, which implement the
|
||||||
a DOM according to the HTML specification, resolving malformed and misplaced
|
tokenization, and tokenization and tree construction stages of the WHATWG HTML
|
||||||
tags where appropriate. The tokenizer simply tokenizes the HTML presented to it,
|
parsing specification respectively. While the tokenizer parses and normalizes
|
||||||
and as such does not resolve issues that may exist in the processed HTML,
|
individual HTML tokens, only the parser constructs the DOM tree from the
|
||||||
producing a literal interpretation of the input.
|
tokenized HTML, as described in the tree construction stage of the
|
||||||
|
specification, dynamically modifying or extending the document's DOM tree.
|
||||||
|
|
||||||
If your use case requires semantically well-formed HTML, as defined by the
|
If your use case requires semantically well-formed HTML documents, as defined by
|
||||||
WHATWG specifiction, the parser should be used rather than the tokenizer.
|
the WHATWG specification, the parser should be used rather than the tokenizer.
|
||||||
|
|
||||||
|
In security contexts, if trust decisions are being made using the tokenized or
|
||||||
|
parsed content, the input must be re-serialized (for instance by using Render or
|
||||||
|
Token.String) in order for those trust decisions to hold, as the process of
|
||||||
|
tokenization or parsing may alter the content.
|
||||||
*/
|
*/
|
||||||
package html // import "golang.org/x/net/html"
|
package html // import "golang.org/x/net/html"
|
||||||
|
|
||||||
|
|||||||
2
vendor/golang.org/x/net/html/doctype.go
generated
vendored
2
vendor/golang.org/x/net/html/doctype.go
generated
vendored
@@ -87,7 +87,7 @@ func parseDoctype(s string) (n *Node, quirks bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lastAttr := n.Attr[len(n.Attr)-1]; lastAttr.Key == "system" &&
|
if lastAttr := n.Attr[len(n.Attr)-1]; lastAttr.Key == "system" &&
|
||||||
strings.ToLower(lastAttr.Val) == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd" {
|
strings.EqualFold(lastAttr.Val, "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") {
|
||||||
quirks = true
|
quirks = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
vendor/golang.org/x/net/html/foreign.go
generated
vendored
3
vendor/golang.org/x/net/html/foreign.go
generated
vendored
@@ -40,8 +40,7 @@ func htmlIntegrationPoint(n *Node) bool {
|
|||||||
if n.Data == "annotation-xml" {
|
if n.Data == "annotation-xml" {
|
||||||
for _, a := range n.Attr {
|
for _, a := range n.Attr {
|
||||||
if a.Key == "encoding" {
|
if a.Key == "encoding" {
|
||||||
val := strings.ToLower(a.Val)
|
if strings.EqualFold(a.Val, "text/html") || strings.EqualFold(a.Val, "application/xhtml+xml") {
|
||||||
if val == "text/html" || val == "application/xhtml+xml" {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
vendor/golang.org/x/net/html/iter.go
generated
vendored
Normal file
56
vendor/golang.org/x/net/html/iter.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2024 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.23
|
||||||
|
|
||||||
|
package html
|
||||||
|
|
||||||
|
import "iter"
|
||||||
|
|
||||||
|
// Ancestors returns an iterator over the ancestors of n, starting with n.Parent.
|
||||||
|
//
|
||||||
|
// Mutating a Node or its parents while iterating may have unexpected results.
|
||||||
|
func (n *Node) Ancestors() iter.Seq[*Node] {
|
||||||
|
_ = n.Parent // eager nil check
|
||||||
|
|
||||||
|
return func(yield func(*Node) bool) {
|
||||||
|
for p := n.Parent; p != nil && yield(p); p = p.Parent {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChildNodes returns an iterator over the immediate children of n,
|
||||||
|
// starting with n.FirstChild.
|
||||||
|
//
|
||||||
|
// Mutating a Node or its children while iterating may have unexpected results.
|
||||||
|
func (n *Node) ChildNodes() iter.Seq[*Node] {
|
||||||
|
_ = n.FirstChild // eager nil check
|
||||||
|
|
||||||
|
return func(yield func(*Node) bool) {
|
||||||
|
for c := n.FirstChild; c != nil && yield(c); c = c.NextSibling {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descendants returns an iterator over all nodes recursively beneath
|
||||||
|
// n, excluding n itself. Nodes are visited in depth-first preorder.
|
||||||
|
//
|
||||||
|
// Mutating a Node or its descendants while iterating may have unexpected results.
|
||||||
|
func (n *Node) Descendants() iter.Seq[*Node] {
|
||||||
|
_ = n.FirstChild // eager nil check
|
||||||
|
|
||||||
|
return func(yield func(*Node) bool) {
|
||||||
|
n.descendants(yield)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) descendants(yield func(*Node) bool) bool {
|
||||||
|
for c := range n.ChildNodes() {
|
||||||
|
if !yield(c) || !c.descendants(yield) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
4
vendor/golang.org/x/net/html/node.go
generated
vendored
4
vendor/golang.org/x/net/html/node.go
generated
vendored
@@ -38,6 +38,10 @@ var scopeMarker = Node{Type: scopeMarkerNode}
|
|||||||
// that it looks like "a<b" rather than "a<b". For element nodes, DataAtom
|
// that it looks like "a<b" rather than "a<b". For element nodes, DataAtom
|
||||||
// is the atom for Data, or zero if Data is not a known tag name.
|
// is the atom for Data, or zero if Data is not a known tag name.
|
||||||
//
|
//
|
||||||
|
// Node trees may be navigated using the link fields (Parent,
|
||||||
|
// FirstChild, and so on) or a range loop over iterators such as
|
||||||
|
// [Node.Descendants].
|
||||||
|
//
|
||||||
// An empty Namespace implies a "http://www.w3.org/1999/xhtml" namespace.
|
// An empty Namespace implies a "http://www.w3.org/1999/xhtml" namespace.
|
||||||
// Similarly, "math" is short for "http://www.w3.org/1998/Math/MathML", and
|
// Similarly, "math" is short for "http://www.w3.org/1998/Math/MathML", and
|
||||||
// "svg" is short for "http://www.w3.org/2000/svg".
|
// "svg" is short for "http://www.w3.org/2000/svg".
|
||||||
|
|||||||
8
vendor/golang.org/x/net/html/parse.go
generated
vendored
8
vendor/golang.org/x/net/html/parse.go
generated
vendored
@@ -840,6 +840,10 @@ func afterHeadIM(p *parser) bool {
|
|||||||
|
|
||||||
p.parseImpliedToken(StartTagToken, a.Body, a.Body.String())
|
p.parseImpliedToken(StartTagToken, a.Body, a.Body.String())
|
||||||
p.framesetOK = true
|
p.framesetOK = true
|
||||||
|
if p.tok.Type == ErrorToken {
|
||||||
|
// Stop parsing.
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1031,7 +1035,7 @@ func inBodyIM(p *parser) bool {
|
|||||||
if p.tok.DataAtom == a.Input {
|
if p.tok.DataAtom == a.Input {
|
||||||
for _, t := range p.tok.Attr {
|
for _, t := range p.tok.Attr {
|
||||||
if t.Key == "type" {
|
if t.Key == "type" {
|
||||||
if strings.ToLower(t.Val) == "hidden" {
|
if strings.EqualFold(t.Val, "hidden") {
|
||||||
// Skip setting framesetOK = false
|
// Skip setting framesetOK = false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -1459,7 +1463,7 @@ func inTableIM(p *parser) bool {
|
|||||||
return inHeadIM(p)
|
return inHeadIM(p)
|
||||||
case a.Input:
|
case a.Input:
|
||||||
for _, t := range p.tok.Attr {
|
for _, t := range p.tok.Attr {
|
||||||
if t.Key == "type" && strings.ToLower(t.Val) == "hidden" {
|
if t.Key == "type" && strings.EqualFold(t.Val, "hidden") {
|
||||||
p.addElement()
|
p.addElement()
|
||||||
p.oe.pop()
|
p.oe.pop()
|
||||||
return true
|
return true
|
||||||
|
|||||||
28
vendor/golang.org/x/net/html/render.go
generated
vendored
28
vendor/golang.org/x/net/html/render.go
generated
vendored
@@ -194,9 +194,8 @@ func render1(w writer, n *Node) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render any child nodes.
|
// Render any child nodes
|
||||||
switch n.Data {
|
if childTextNodesAreLiteral(n) {
|
||||||
case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp":
|
|
||||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
if c.Type == TextNode {
|
if c.Type == TextNode {
|
||||||
if _, err := w.WriteString(c.Data); err != nil {
|
if _, err := w.WriteString(c.Data); err != nil {
|
||||||
@@ -213,7 +212,7 @@ func render1(w writer, n *Node) error {
|
|||||||
// last element in the file, with no closing tag.
|
// last element in the file, with no closing tag.
|
||||||
return plaintextAbort
|
return plaintextAbort
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
if err := render1(w, c); err != nil {
|
if err := render1(w, c); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -231,6 +230,27 @@ func render1(w writer, n *Node) error {
|
|||||||
return w.WriteByte('>')
|
return w.WriteByte('>')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func childTextNodesAreLiteral(n *Node) bool {
|
||||||
|
// Per WHATWG HTML 13.3, if the parent of the current node is a style,
|
||||||
|
// script, xmp, iframe, noembed, noframes, or plaintext element, and the
|
||||||
|
// current node is a text node, append the value of the node's data
|
||||||
|
// literally. The specification is not explicit about it, but we only
|
||||||
|
// enforce this if we are in the HTML namespace (i.e. when the namespace is
|
||||||
|
// "").
|
||||||
|
// NOTE: we also always include noscript elements, although the
|
||||||
|
// specification states that they should only be rendered as such if
|
||||||
|
// scripting is enabled for the node (which is not something we track).
|
||||||
|
if n.Namespace != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch n.Data {
|
||||||
|
case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// writeQuoted writes s to w surrounded by quotes. Normally it will use double
|
// writeQuoted writes s to w surrounded by quotes. Normally it will use double
|
||||||
// quotes, but if s contains a double quote, it will use single quotes.
|
// quotes, but if s contains a double quote, it will use single quotes.
|
||||||
// It is used for writing the identifiers in a doctype declaration.
|
// It is used for writing the identifiers in a doctype declaration.
|
||||||
|
|||||||
19
vendor/golang.org/x/net/html/token.go
generated
vendored
19
vendor/golang.org/x/net/html/token.go
generated
vendored
@@ -910,10 +910,16 @@ func (z *Tokenizer) readTagAttrKey() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch c {
|
switch c {
|
||||||
case ' ', '\n', '\r', '\t', '\f', '/':
|
case '=':
|
||||||
z.pendingAttr[0].end = z.raw.end - 1
|
if z.pendingAttr[0].start+1 == z.raw.end {
|
||||||
return
|
// WHATWG 13.2.5.32, if we see an equals sign before the attribute name
|
||||||
case '=', '>':
|
// begins, we treat it as a character in the attribute name and continue.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case ' ', '\n', '\r', '\t', '\f', '/', '>':
|
||||||
|
// WHATWG 13.2.5.33 Attribute name state
|
||||||
|
// We need to reconsume the char in the after attribute name state to support the / character
|
||||||
z.raw.end--
|
z.raw.end--
|
||||||
z.pendingAttr[0].end = z.raw.end
|
z.pendingAttr[0].end = z.raw.end
|
||||||
return
|
return
|
||||||
@@ -932,6 +938,11 @@ func (z *Tokenizer) readTagAttrVal() {
|
|||||||
if z.err != nil {
|
if z.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if c == '/' {
|
||||||
|
// WHATWG 13.2.5.34 After attribute name state
|
||||||
|
// U+002F SOLIDUS (/) - Switch to the self-closing start tag state.
|
||||||
|
return
|
||||||
|
}
|
||||||
if c != '=' {
|
if c != '=' {
|
||||||
z.raw.end--
|
z.raw.end--
|
||||||
return
|
return
|
||||||
|
|||||||
4
vendor/golang.org/x/sys/LICENSE
generated
vendored
4
vendor/golang.org/x/sys/LICENSE
generated
vendored
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
Copyright 2009 The Go Authors.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are
|
||||||
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
|
|||||||
copyright notice, this list of conditions and the following disclaimer
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
in the documentation and/or other materials provided with the
|
in the documentation and/or other materials provided with the
|
||||||
distribution.
|
distribution.
|
||||||
* Neither the name of Google Inc. nor the names of its
|
* Neither the name of Google LLC nor the names of its
|
||||||
contributors may be used to endorse or promote products derived from
|
contributors may be used to endorse or promote products derived from
|
||||||
this software without specific prior written permission.
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
|||||||
30
vendor/golang.org/x/sys/internal/unsafeheader/unsafeheader.go
generated
vendored
30
vendor/golang.org/x/sys/internal/unsafeheader/unsafeheader.go
generated
vendored
@@ -1,30 +0,0 @@
|
|||||||
// Copyright 2020 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package unsafeheader contains header declarations for the Go runtime's
|
|
||||||
// slice and string implementations.
|
|
||||||
//
|
|
||||||
// This package allows x/sys to use types equivalent to
|
|
||||||
// reflect.SliceHeader and reflect.StringHeader without introducing
|
|
||||||
// a dependency on the (relatively heavy) "reflect" package.
|
|
||||||
package unsafeheader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Slice is the runtime representation of a slice.
|
|
||||||
// It cannot be used safely or portably and its representation may change in a later release.
|
|
||||||
type Slice struct {
|
|
||||||
Data unsafe.Pointer
|
|
||||||
Len int
|
|
||||||
Cap int
|
|
||||||
}
|
|
||||||
|
|
||||||
// String is the runtime representation of a string.
|
|
||||||
// It cannot be used safely or portably and its representation may change in a later release.
|
|
||||||
type String struct {
|
|
||||||
Data unsafe.Pointer
|
|
||||||
Len int
|
|
||||||
}
|
|
||||||
3
vendor/golang.org/x/sys/windows/aliases.go
generated
vendored
3
vendor/golang.org/x/sys/windows/aliases.go
generated
vendored
@@ -2,8 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build windows && go1.9
|
//go:build windows
|
||||||
// +build windows,go1.9
|
|
||||||
|
|
||||||
package windows
|
package windows
|
||||||
|
|
||||||
|
|||||||
13
vendor/golang.org/x/sys/windows/dll_windows.go
generated
vendored
13
vendor/golang.org/x/sys/windows/dll_windows.go
generated
vendored
@@ -43,8 +43,8 @@ type DLL struct {
|
|||||||
// LoadDLL loads DLL file into memory.
|
// LoadDLL loads DLL file into memory.
|
||||||
//
|
//
|
||||||
// Warning: using LoadDLL without an absolute path name is subject to
|
// Warning: using LoadDLL without an absolute path name is subject to
|
||||||
// DLL preloading attacks. To safely load a system DLL, use LazyDLL
|
// DLL preloading attacks. To safely load a system DLL, use [NewLazySystemDLL],
|
||||||
// with System set to true, or use LoadLibraryEx directly.
|
// or use [LoadLibraryEx] directly.
|
||||||
func LoadDLL(name string) (dll *DLL, err error) {
|
func LoadDLL(name string) (dll *DLL, err error) {
|
||||||
namep, err := UTF16PtrFromString(name)
|
namep, err := UTF16PtrFromString(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,7 +65,7 @@ func LoadDLL(name string) (dll *DLL, err error) {
|
|||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustLoadDLL is like LoadDLL but panics if load operation failes.
|
// MustLoadDLL is like LoadDLL but panics if load operation fails.
|
||||||
func MustLoadDLL(name string) *DLL {
|
func MustLoadDLL(name string) *DLL {
|
||||||
d, e := LoadDLL(name)
|
d, e := LoadDLL(name)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
@@ -271,6 +271,9 @@ func (d *LazyDLL) NewProc(name string) *LazyProc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewLazyDLL creates new LazyDLL associated with DLL file.
|
// NewLazyDLL creates new LazyDLL associated with DLL file.
|
||||||
|
//
|
||||||
|
// Warning: using NewLazyDLL without an absolute path name is subject to
|
||||||
|
// DLL preloading attacks. To safely load a system DLL, use [NewLazySystemDLL].
|
||||||
func NewLazyDLL(name string) *LazyDLL {
|
func NewLazyDLL(name string) *LazyDLL {
|
||||||
return &LazyDLL{Name: name}
|
return &LazyDLL{Name: name}
|
||||||
}
|
}
|
||||||
@@ -410,7 +413,3 @@ func loadLibraryEx(name string, system bool) (*DLL, error) {
|
|||||||
}
|
}
|
||||||
return &DLL{Name: name, Handle: h}, nil
|
return &DLL{Name: name, Handle: h}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type errString string
|
|
||||||
|
|
||||||
func (s errString) Error() string { return string(s) }
|
|
||||||
|
|||||||
9
vendor/golang.org/x/sys/windows/empty.s
generated
vendored
9
vendor/golang.org/x/sys/windows/empty.s
generated
vendored
@@ -1,9 +0,0 @@
|
|||||||
// Copyright 2019 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build !go1.12
|
|
||||||
// +build !go1.12
|
|
||||||
|
|
||||||
// This file is here to allow bodyless functions with go:linkname for Go 1.11
|
|
||||||
// and earlier (see https://golang.org/issue/23311).
|
|
||||||
17
vendor/golang.org/x/sys/windows/env_windows.go
generated
vendored
17
vendor/golang.org/x/sys/windows/env_windows.go
generated
vendored
@@ -37,14 +37,17 @@ func (token Token) Environ(inheritExisting bool) (env []string, err error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer DestroyEnvironmentBlock(block)
|
defer DestroyEnvironmentBlock(block)
|
||||||
blockp := uintptr(unsafe.Pointer(block))
|
size := unsafe.Sizeof(*block)
|
||||||
for {
|
for *block != 0 {
|
||||||
entry := UTF16PtrToString((*uint16)(unsafe.Pointer(blockp)))
|
// find NUL terminator
|
||||||
if len(entry) == 0 {
|
end := unsafe.Pointer(block)
|
||||||
break
|
for *(*uint16)(end) != 0 {
|
||||||
|
end = unsafe.Add(end, size)
|
||||||
}
|
}
|
||||||
env = append(env, entry)
|
|
||||||
blockp += 2 * (uintptr(len(entry)) + 1)
|
entry := unsafe.Slice(block, (uintptr(end)-uintptr(unsafe.Pointer(block)))/size)
|
||||||
|
env = append(env, UTF16ToString(entry))
|
||||||
|
block = (*uint16)(unsafe.Add(end, size))
|
||||||
}
|
}
|
||||||
return env, nil
|
return env, nil
|
||||||
}
|
}
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/windows/eventlog.go
generated
vendored
1
vendor/golang.org/x/sys/windows/eventlog.go
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package windows
|
package windows
|
||||||
|
|
||||||
|
|||||||
94
vendor/golang.org/x/sys/windows/exec_windows.go
generated
vendored
94
vendor/golang.org/x/sys/windows/exec_windows.go
generated
vendored
@@ -22,7 +22,7 @@ import (
|
|||||||
// but only if there is space or tab inside s.
|
// but only if there is space or tab inside s.
|
||||||
func EscapeArg(s string) string {
|
func EscapeArg(s string) string {
|
||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
return "\"\""
|
return `""`
|
||||||
}
|
}
|
||||||
n := len(s)
|
n := len(s)
|
||||||
hasSpace := false
|
hasSpace := false
|
||||||
@@ -35,7 +35,7 @@ func EscapeArg(s string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasSpace {
|
if hasSpace {
|
||||||
n += 2
|
n += 2 // Reserve space for quotes.
|
||||||
}
|
}
|
||||||
if n == len(s) {
|
if n == len(s) {
|
||||||
return s
|
return s
|
||||||
@@ -82,36 +82,106 @@ func EscapeArg(s string) string {
|
|||||||
// in CreateProcess's CommandLine argument, CreateService/ChangeServiceConfig's BinaryPathName argument,
|
// in CreateProcess's CommandLine argument, CreateService/ChangeServiceConfig's BinaryPathName argument,
|
||||||
// or any program that uses CommandLineToArgv.
|
// or any program that uses CommandLineToArgv.
|
||||||
func ComposeCommandLine(args []string) string {
|
func ComposeCommandLine(args []string) string {
|
||||||
var commandLine string
|
if len(args) == 0 {
|
||||||
for i := range args {
|
return ""
|
||||||
if i > 0 {
|
|
||||||
commandLine += " "
|
|
||||||
}
|
|
||||||
commandLine += EscapeArg(args[i])
|
|
||||||
}
|
}
|
||||||
return commandLine
|
|
||||||
|
// Per https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw:
|
||||||
|
// “This function accepts command lines that contain a program name; the
|
||||||
|
// program name can be enclosed in quotation marks or not.”
|
||||||
|
//
|
||||||
|
// Unfortunately, it provides no means of escaping interior quotation marks
|
||||||
|
// within that program name, and we have no way to report them here.
|
||||||
|
prog := args[0]
|
||||||
|
mustQuote := len(prog) == 0
|
||||||
|
for i := 0; i < len(prog); i++ {
|
||||||
|
c := prog[i]
|
||||||
|
if c <= ' ' || (c == '"' && i == 0) {
|
||||||
|
// Force quotes for not only the ASCII space and tab as described in the
|
||||||
|
// MSDN article, but also ASCII control characters.
|
||||||
|
// The documentation for CommandLineToArgvW doesn't say what happens when
|
||||||
|
// the first argument is not a valid program name, but it empirically
|
||||||
|
// seems to drop unquoted control characters.
|
||||||
|
mustQuote = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var commandLine []byte
|
||||||
|
if mustQuote {
|
||||||
|
commandLine = make([]byte, 0, len(prog)+2)
|
||||||
|
commandLine = append(commandLine, '"')
|
||||||
|
for i := 0; i < len(prog); i++ {
|
||||||
|
c := prog[i]
|
||||||
|
if c == '"' {
|
||||||
|
// This quote would interfere with our surrounding quotes.
|
||||||
|
// We have no way to report an error, so just strip out
|
||||||
|
// the offending character instead.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
commandLine = append(commandLine, c)
|
||||||
|
}
|
||||||
|
commandLine = append(commandLine, '"')
|
||||||
|
} else {
|
||||||
|
if len(args) == 1 {
|
||||||
|
// args[0] is a valid command line representing itself.
|
||||||
|
// No need to allocate a new slice or string for it.
|
||||||
|
return prog
|
||||||
|
}
|
||||||
|
commandLine = []byte(prog)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range args[1:] {
|
||||||
|
commandLine = append(commandLine, ' ')
|
||||||
|
// TODO(bcmills): since we're already appending to a slice, it would be nice
|
||||||
|
// to avoid the intermediate allocations of EscapeArg.
|
||||||
|
// Perhaps we can factor out an appendEscapedArg function.
|
||||||
|
commandLine = append(commandLine, EscapeArg(arg)...)
|
||||||
|
}
|
||||||
|
return string(commandLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecomposeCommandLine breaks apart its argument command line into unescaped parts using CommandLineToArgv,
|
// DecomposeCommandLine breaks apart its argument command line into unescaped parts using CommandLineToArgv,
|
||||||
// as gathered from GetCommandLine, QUERY_SERVICE_CONFIG's BinaryPathName argument, or elsewhere that
|
// as gathered from GetCommandLine, QUERY_SERVICE_CONFIG's BinaryPathName argument, or elsewhere that
|
||||||
// command lines are passed around.
|
// command lines are passed around.
|
||||||
|
// DecomposeCommandLine returns an error if commandLine contains NUL.
|
||||||
func DecomposeCommandLine(commandLine string) ([]string, error) {
|
func DecomposeCommandLine(commandLine string) ([]string, error) {
|
||||||
if len(commandLine) == 0 {
|
if len(commandLine) == 0 {
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
utf16CommandLine, err := UTF16FromString(commandLine)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorspkg.New("string with NUL passed to DecomposeCommandLine")
|
||||||
|
}
|
||||||
var argc int32
|
var argc int32
|
||||||
argv, err := CommandLineToArgv(StringToUTF16Ptr(commandLine), &argc)
|
argv, err := commandLineToArgv(&utf16CommandLine[0], &argc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer LocalFree(Handle(unsafe.Pointer(argv)))
|
defer LocalFree(Handle(unsafe.Pointer(argv)))
|
||||||
|
|
||||||
var args []string
|
var args []string
|
||||||
for _, v := range (*argv)[:argc] {
|
for _, p := range unsafe.Slice(argv, argc) {
|
||||||
args = append(args, UTF16ToString((*v)[:]))
|
args = append(args, UTF16PtrToString(p))
|
||||||
}
|
}
|
||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommandLineToArgv parses a Unicode command line string and sets
|
||||||
|
// argc to the number of parsed arguments.
|
||||||
|
//
|
||||||
|
// The returned memory should be freed using a single call to LocalFree.
|
||||||
|
//
|
||||||
|
// Note that although the return type of CommandLineToArgv indicates 8192
|
||||||
|
// entries of up to 8192 characters each, the actual count of parsed arguments
|
||||||
|
// may exceed 8192, and the documentation for CommandLineToArgvW does not mention
|
||||||
|
// any bound on the lengths of the individual argument strings.
|
||||||
|
// (See https://go.dev/issue/63236.)
|
||||||
|
func CommandLineToArgv(cmd *uint16, argc *int32) (argv *[8192]*[8192]uint16, err error) {
|
||||||
|
argp, err := commandLineToArgv(cmd, argc)
|
||||||
|
argv = (*[8192]*[8192]uint16)(unsafe.Pointer(argp))
|
||||||
|
return argv, err
|
||||||
|
}
|
||||||
|
|
||||||
func CloseOnExec(fd Handle) {
|
func CloseOnExec(fd Handle) {
|
||||||
SetHandleInformation(Handle(fd), HANDLE_FLAG_INHERIT, 0)
|
SetHandleInformation(Handle(fd), HANDLE_FLAG_INHERIT, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/windows/mksyscall.go
generated
vendored
1
vendor/golang.org/x/sys/windows/mksyscall.go
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build generate
|
//go:build generate
|
||||||
// +build generate
|
|
||||||
|
|
||||||
package windows
|
package windows
|
||||||
|
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/windows/race.go
generated
vendored
1
vendor/golang.org/x/sys/windows/race.go
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build windows && race
|
//go:build windows && race
|
||||||
// +build windows,race
|
|
||||||
|
|
||||||
package windows
|
package windows
|
||||||
|
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/windows/race0.go
generated
vendored
1
vendor/golang.org/x/sys/windows/race0.go
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build windows && !race
|
//go:build windows && !race
|
||||||
// +build windows,!race
|
|
||||||
|
|
||||||
package windows
|
package windows
|
||||||
|
|
||||||
|
|||||||
46
vendor/golang.org/x/sys/windows/security_windows.go
generated
vendored
46
vendor/golang.org/x/sys/windows/security_windows.go
generated
vendored
@@ -7,8 +7,6 @@ package windows
|
|||||||
import (
|
import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"golang.org/x/sys/internal/unsafeheader"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -70,6 +68,7 @@ type UserInfo10 struct {
|
|||||||
//sys NetUserGetInfo(serverName *uint16, userName *uint16, level uint32, buf **byte) (neterr error) = netapi32.NetUserGetInfo
|
//sys NetUserGetInfo(serverName *uint16, userName *uint16, level uint32, buf **byte) (neterr error) = netapi32.NetUserGetInfo
|
||||||
//sys NetGetJoinInformation(server *uint16, name **uint16, bufType *uint32) (neterr error) = netapi32.NetGetJoinInformation
|
//sys NetGetJoinInformation(server *uint16, name **uint16, bufType *uint32) (neterr error) = netapi32.NetGetJoinInformation
|
||||||
//sys NetApiBufferFree(buf *byte) (neterr error) = netapi32.NetApiBufferFree
|
//sys NetApiBufferFree(buf *byte) (neterr error) = netapi32.NetApiBufferFree
|
||||||
|
//sys NetUserEnum(serverName *uint16, level uint32, filter uint32, buf **byte, prefMaxLen uint32, entriesRead *uint32, totalEntries *uint32, resumeHandle *uint32) (neterr error) = netapi32.NetUserEnum
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// do not reorder
|
// do not reorder
|
||||||
@@ -895,7 +894,7 @@ type ACL struct {
|
|||||||
aclRevision byte
|
aclRevision byte
|
||||||
sbz1 byte
|
sbz1 byte
|
||||||
aclSize uint16
|
aclSize uint16
|
||||||
aceCount uint16
|
AceCount uint16
|
||||||
sbz2 uint16
|
sbz2 uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1088,6 +1087,27 @@ type EXPLICIT_ACCESS struct {
|
|||||||
Trustee TRUSTEE
|
Trustee TRUSTEE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-ace_header
|
||||||
|
type ACE_HEADER struct {
|
||||||
|
AceType uint8
|
||||||
|
AceFlags uint8
|
||||||
|
AceSize uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-access_allowed_ace
|
||||||
|
type ACCESS_ALLOWED_ACE struct {
|
||||||
|
Header ACE_HEADER
|
||||||
|
Mask ACCESS_MASK
|
||||||
|
SidStart uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Constants for AceType
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-ace_header
|
||||||
|
ACCESS_ALLOWED_ACE_TYPE = 0
|
||||||
|
ACCESS_DENIED_ACE_TYPE = 1
|
||||||
|
)
|
||||||
|
|
||||||
// This type is the union inside of TRUSTEE and must be created using one of the TrusteeValueFrom* functions.
|
// This type is the union inside of TRUSTEE and must be created using one of the TrusteeValueFrom* functions.
|
||||||
type TrusteeValue uintptr
|
type TrusteeValue uintptr
|
||||||
|
|
||||||
@@ -1159,6 +1179,7 @@ type OBJECTS_AND_NAME struct {
|
|||||||
//sys makeSelfRelativeSD(absoluteSD *SECURITY_DESCRIPTOR, selfRelativeSD *SECURITY_DESCRIPTOR, selfRelativeSDSize *uint32) (err error) = advapi32.MakeSelfRelativeSD
|
//sys makeSelfRelativeSD(absoluteSD *SECURITY_DESCRIPTOR, selfRelativeSD *SECURITY_DESCRIPTOR, selfRelativeSDSize *uint32) (err error) = advapi32.MakeSelfRelativeSD
|
||||||
|
|
||||||
//sys setEntriesInAcl(countExplicitEntries uint32, explicitEntries *EXPLICIT_ACCESS, oldACL *ACL, newACL **ACL) (ret error) = advapi32.SetEntriesInAclW
|
//sys setEntriesInAcl(countExplicitEntries uint32, explicitEntries *EXPLICIT_ACCESS, oldACL *ACL, newACL **ACL) (ret error) = advapi32.SetEntriesInAclW
|
||||||
|
//sys GetAce(acl *ACL, aceIndex uint32, pAce **ACCESS_ALLOWED_ACE) (err error) = advapi32.GetAce
|
||||||
|
|
||||||
// Control returns the security descriptor control bits.
|
// Control returns the security descriptor control bits.
|
||||||
func (sd *SECURITY_DESCRIPTOR) Control() (control SECURITY_DESCRIPTOR_CONTROL, revision uint32, err error) {
|
func (sd *SECURITY_DESCRIPTOR) Control() (control SECURITY_DESCRIPTOR_CONTROL, revision uint32, err error) {
|
||||||
@@ -1341,21 +1362,14 @@ func (selfRelativeSD *SECURITY_DESCRIPTOR) copySelfRelativeSecurityDescriptor()
|
|||||||
sdLen = min
|
sdLen = min
|
||||||
}
|
}
|
||||||
|
|
||||||
var src []byte
|
src := unsafe.Slice((*byte)(unsafe.Pointer(selfRelativeSD)), sdLen)
|
||||||
h := (*unsafeheader.Slice)(unsafe.Pointer(&src))
|
// SECURITY_DESCRIPTOR has pointers in it, which means checkptr expects for it to
|
||||||
h.Data = unsafe.Pointer(selfRelativeSD)
|
// be aligned properly. When we're copying a Windows-allocated struct to a
|
||||||
h.Len = sdLen
|
// Go-allocated one, make sure that the Go allocation is aligned to the
|
||||||
h.Cap = sdLen
|
// pointer size.
|
||||||
|
|
||||||
const psize = int(unsafe.Sizeof(uintptr(0)))
|
const psize = int(unsafe.Sizeof(uintptr(0)))
|
||||||
|
|
||||||
var dst []byte
|
|
||||||
h = (*unsafeheader.Slice)(unsafe.Pointer(&dst))
|
|
||||||
alloc := make([]uintptr, (sdLen+psize-1)/psize)
|
alloc := make([]uintptr, (sdLen+psize-1)/psize)
|
||||||
h.Data = (*unsafeheader.Slice)(unsafe.Pointer(&alloc)).Data
|
dst := unsafe.Slice((*byte)(unsafe.Pointer(&alloc[0])), sdLen)
|
||||||
h.Len = sdLen
|
|
||||||
h.Cap = sdLen
|
|
||||||
|
|
||||||
copy(dst, src)
|
copy(dst, src)
|
||||||
return (*SECURITY_DESCRIPTOR)(unsafe.Pointer(&dst[0]))
|
return (*SECURITY_DESCRIPTOR)(unsafe.Pointer(&dst[0]))
|
||||||
}
|
}
|
||||||
|
|||||||
12
vendor/golang.org/x/sys/windows/service.go
generated
vendored
12
vendor/golang.org/x/sys/windows/service.go
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package windows
|
package windows
|
||||||
|
|
||||||
@@ -141,6 +140,12 @@ const (
|
|||||||
SERVICE_DYNAMIC_INFORMATION_LEVEL_START_REASON = 1
|
SERVICE_DYNAMIC_INFORMATION_LEVEL_START_REASON = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ENUM_SERVICE_STATUS struct {
|
||||||
|
ServiceName *uint16
|
||||||
|
DisplayName *uint16
|
||||||
|
ServiceStatus SERVICE_STATUS
|
||||||
|
}
|
||||||
|
|
||||||
type SERVICE_STATUS struct {
|
type SERVICE_STATUS struct {
|
||||||
ServiceType uint32
|
ServiceType uint32
|
||||||
CurrentState uint32
|
CurrentState uint32
|
||||||
@@ -212,6 +217,10 @@ type SERVICE_FAILURE_ACTIONS struct {
|
|||||||
Actions *SC_ACTION
|
Actions *SC_ACTION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SERVICE_FAILURE_ACTIONS_FLAG struct {
|
||||||
|
FailureActionsOnNonCrashFailures int32
|
||||||
|
}
|
||||||
|
|
||||||
type SC_ACTION struct {
|
type SC_ACTION struct {
|
||||||
Type uint32
|
Type uint32
|
||||||
Delay uint32
|
Delay uint32
|
||||||
@@ -245,3 +254,4 @@ type QUERY_SERVICE_LOCK_STATUS struct {
|
|||||||
//sys UnsubscribeServiceChangeNotifications(subscription uintptr) = sechost.UnsubscribeServiceChangeNotifications?
|
//sys UnsubscribeServiceChangeNotifications(subscription uintptr) = sechost.UnsubscribeServiceChangeNotifications?
|
||||||
//sys RegisterServiceCtrlHandlerEx(serviceName *uint16, handlerProc uintptr, context uintptr) (handle Handle, err error) = advapi32.RegisterServiceCtrlHandlerExW
|
//sys RegisterServiceCtrlHandlerEx(serviceName *uint16, handlerProc uintptr, context uintptr) (handle Handle, err error) = advapi32.RegisterServiceCtrlHandlerExW
|
||||||
//sys QueryServiceDynamicInformation(service Handle, infoLevel uint32, dynamicInfo unsafe.Pointer) (err error) = advapi32.QueryServiceDynamicInformation?
|
//sys QueryServiceDynamicInformation(service Handle, infoLevel uint32, dynamicInfo unsafe.Pointer) (err error) = advapi32.QueryServiceDynamicInformation?
|
||||||
|
//sys EnumDependentServices(service Handle, activityState uint32, services *ENUM_SERVICE_STATUS, buffSize uint32, bytesNeeded *uint32, servicesReturned *uint32) (err error) = advapi32.EnumDependentServicesW
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/windows/str.go
generated
vendored
1
vendor/golang.org/x/sys/windows/str.go
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package windows
|
package windows
|
||||||
|
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/windows/syscall.go
generated
vendored
1
vendor/golang.org/x/sys/windows/syscall.go
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
|
||||||
|
|
||||||
// Package windows contains an interface to the low-level operating system
|
// Package windows contains an interface to the low-level operating system
|
||||||
// primitives. OS details vary depending on the underlying system, and
|
// primitives. OS details vary depending on the underlying system, and
|
||||||
|
|||||||
214
vendor/golang.org/x/sys/windows/syscall_windows.go
generated
vendored
214
vendor/golang.org/x/sys/windows/syscall_windows.go
generated
vendored
@@ -15,12 +15,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"golang.org/x/sys/internal/unsafeheader"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handle uintptr
|
type (
|
||||||
type HWND uintptr
|
Handle uintptr
|
||||||
|
HWND uintptr
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
InvalidHandle = ^Handle(0)
|
InvalidHandle = ^Handle(0)
|
||||||
@@ -127,22 +127,21 @@ func UTF16PtrToString(p *uint16) string {
|
|||||||
for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ {
|
for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ {
|
||||||
ptr = unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(*p))
|
ptr = unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(*p))
|
||||||
}
|
}
|
||||||
|
return UTF16ToString(unsafe.Slice(p, n))
|
||||||
return string(utf16.Decode(unsafe.Slice(p, n)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Getpagesize() int { return 4096 }
|
func Getpagesize() int { return 4096 }
|
||||||
|
|
||||||
// NewCallback converts a Go function to a function pointer conforming to the stdcall calling convention.
|
// NewCallback converts a Go function to a function pointer conforming to the stdcall calling convention.
|
||||||
// This is useful when interoperating with Windows code requiring callbacks.
|
// This is useful when interoperating with Windows code requiring callbacks.
|
||||||
// The argument is expected to be a function with with one uintptr-sized result. The function must not have arguments with size larger than the size of uintptr.
|
// The argument is expected to be a function with one uintptr-sized result. The function must not have arguments with size larger than the size of uintptr.
|
||||||
func NewCallback(fn interface{}) uintptr {
|
func NewCallback(fn interface{}) uintptr {
|
||||||
return syscall.NewCallback(fn)
|
return syscall.NewCallback(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCallbackCDecl converts a Go function to a function pointer conforming to the cdecl calling convention.
|
// NewCallbackCDecl converts a Go function to a function pointer conforming to the cdecl calling convention.
|
||||||
// This is useful when interoperating with Windows code requiring callbacks.
|
// This is useful when interoperating with Windows code requiring callbacks.
|
||||||
// The argument is expected to be a function with with one uintptr-sized result. The function must not have arguments with size larger than the size of uintptr.
|
// The argument is expected to be a function with one uintptr-sized result. The function must not have arguments with size larger than the size of uintptr.
|
||||||
func NewCallbackCDecl(fn interface{}) uintptr {
|
func NewCallbackCDecl(fn interface{}) uintptr {
|
||||||
return syscall.NewCallbackCDecl(fn)
|
return syscall.NewCallbackCDecl(fn)
|
||||||
}
|
}
|
||||||
@@ -157,6 +156,8 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
|||||||
//sys GetModuleFileName(module Handle, filename *uint16, size uint32) (n uint32, err error) = kernel32.GetModuleFileNameW
|
//sys GetModuleFileName(module Handle, filename *uint16, size uint32) (n uint32, err error) = kernel32.GetModuleFileNameW
|
||||||
//sys GetModuleHandleEx(flags uint32, moduleName *uint16, module *Handle) (err error) = kernel32.GetModuleHandleExW
|
//sys GetModuleHandleEx(flags uint32, moduleName *uint16, module *Handle) (err error) = kernel32.GetModuleHandleExW
|
||||||
//sys SetDefaultDllDirectories(directoryFlags uint32) (err error)
|
//sys SetDefaultDllDirectories(directoryFlags uint32) (err error)
|
||||||
|
//sys AddDllDirectory(path *uint16) (cookie uintptr, err error) = kernel32.AddDllDirectory
|
||||||
|
//sys RemoveDllDirectory(cookie uintptr) (err error) = kernel32.RemoveDllDirectory
|
||||||
//sys SetDllDirectory(path string) (err error) = kernel32.SetDllDirectoryW
|
//sys SetDllDirectory(path string) (err error) = kernel32.SetDllDirectoryW
|
||||||
//sys GetVersion() (ver uint32, err error)
|
//sys GetVersion() (ver uint32, err error)
|
||||||
//sys FormatMessage(flags uint32, msgsrc uintptr, msgid uint32, langid uint32, buf []uint16, args *byte) (n uint32, err error) = FormatMessageW
|
//sys FormatMessage(flags uint32, msgsrc uintptr, msgid uint32, langid uint32, buf []uint16, args *byte) (n uint32, err error) = FormatMessageW
|
||||||
@@ -166,6 +167,9 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
|||||||
//sys CreateFile(name *uint16, access uint32, mode uint32, sa *SecurityAttributes, createmode uint32, attrs uint32, templatefile Handle) (handle Handle, err error) [failretval==InvalidHandle] = CreateFileW
|
//sys CreateFile(name *uint16, access uint32, mode uint32, sa *SecurityAttributes, createmode uint32, attrs uint32, templatefile Handle) (handle Handle, err error) [failretval==InvalidHandle] = CreateFileW
|
||||||
//sys CreateNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *SecurityAttributes) (handle Handle, err error) [failretval==InvalidHandle] = CreateNamedPipeW
|
//sys CreateNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *SecurityAttributes) (handle Handle, err error) [failretval==InvalidHandle] = CreateNamedPipeW
|
||||||
//sys ConnectNamedPipe(pipe Handle, overlapped *Overlapped) (err error)
|
//sys ConnectNamedPipe(pipe Handle, overlapped *Overlapped) (err error)
|
||||||
|
//sys DisconnectNamedPipe(pipe Handle) (err error)
|
||||||
|
//sys GetNamedPipeClientProcessId(pipe Handle, clientProcessID *uint32) (err error)
|
||||||
|
//sys GetNamedPipeServerProcessId(pipe Handle, serverProcessID *uint32) (err error)
|
||||||
//sys GetNamedPipeInfo(pipe Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error)
|
//sys GetNamedPipeInfo(pipe Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error)
|
||||||
//sys GetNamedPipeHandleState(pipe Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
|
//sys GetNamedPipeHandleState(pipe Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
|
||||||
//sys SetNamedPipeHandleState(pipe Handle, state *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32) (err error) = SetNamedPipeHandleState
|
//sys SetNamedPipeHandleState(pipe Handle, state *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32) (err error) = SetNamedPipeHandleState
|
||||||
@@ -194,6 +198,7 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
|||||||
//sys GetComputerName(buf *uint16, n *uint32) (err error) = GetComputerNameW
|
//sys GetComputerName(buf *uint16, n *uint32) (err error) = GetComputerNameW
|
||||||
//sys GetComputerNameEx(nametype uint32, buf *uint16, n *uint32) (err error) = GetComputerNameExW
|
//sys GetComputerNameEx(nametype uint32, buf *uint16, n *uint32) (err error) = GetComputerNameExW
|
||||||
//sys SetEndOfFile(handle Handle) (err error)
|
//sys SetEndOfFile(handle Handle) (err error)
|
||||||
|
//sys SetFileValidData(handle Handle, validDataLength int64) (err error)
|
||||||
//sys GetSystemTimeAsFileTime(time *Filetime)
|
//sys GetSystemTimeAsFileTime(time *Filetime)
|
||||||
//sys GetSystemTimePreciseAsFileTime(time *Filetime)
|
//sys GetSystemTimePreciseAsFileTime(time *Filetime)
|
||||||
//sys GetTimeZoneInformation(tzi *Timezoneinformation) (rc uint32, err error) [failretval==0xffffffff]
|
//sys GetTimeZoneInformation(tzi *Timezoneinformation) (rc uint32, err error) [failretval==0xffffffff]
|
||||||
@@ -210,13 +215,17 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
|||||||
//sys OpenProcess(desiredAccess uint32, inheritHandle bool, processId uint32) (handle Handle, err error)
|
//sys OpenProcess(desiredAccess uint32, inheritHandle bool, processId uint32) (handle Handle, err error)
|
||||||
//sys ShellExecute(hwnd Handle, verb *uint16, file *uint16, args *uint16, cwd *uint16, showCmd int32) (err error) [failretval<=32] = shell32.ShellExecuteW
|
//sys ShellExecute(hwnd Handle, verb *uint16, file *uint16, args *uint16, cwd *uint16, showCmd int32) (err error) [failretval<=32] = shell32.ShellExecuteW
|
||||||
//sys GetWindowThreadProcessId(hwnd HWND, pid *uint32) (tid uint32, err error) = user32.GetWindowThreadProcessId
|
//sys GetWindowThreadProcessId(hwnd HWND, pid *uint32) (tid uint32, err error) = user32.GetWindowThreadProcessId
|
||||||
|
//sys LoadKeyboardLayout(name *uint16, flags uint32) (hkl Handle, err error) [failretval==0] = user32.LoadKeyboardLayoutW
|
||||||
|
//sys UnloadKeyboardLayout(hkl Handle) (err error) = user32.UnloadKeyboardLayout
|
||||||
|
//sys GetKeyboardLayout(tid uint32) (hkl Handle) = user32.GetKeyboardLayout
|
||||||
|
//sys ToUnicodeEx(vkey uint32, scancode uint32, keystate *byte, pwszBuff *uint16, cchBuff int32, flags uint32, hkl Handle) (ret int32) = user32.ToUnicodeEx
|
||||||
//sys GetShellWindow() (shellWindow HWND) = user32.GetShellWindow
|
//sys GetShellWindow() (shellWindow HWND) = user32.GetShellWindow
|
||||||
//sys MessageBox(hwnd HWND, text *uint16, caption *uint16, boxtype uint32) (ret int32, err error) [failretval==0] = user32.MessageBoxW
|
//sys MessageBox(hwnd HWND, text *uint16, caption *uint16, boxtype uint32) (ret int32, err error) [failretval==0] = user32.MessageBoxW
|
||||||
//sys ExitWindowsEx(flags uint32, reason uint32) (err error) = user32.ExitWindowsEx
|
//sys ExitWindowsEx(flags uint32, reason uint32) (err error) = user32.ExitWindowsEx
|
||||||
//sys shGetKnownFolderPath(id *KNOWNFOLDERID, flags uint32, token Token, path **uint16) (ret error) = shell32.SHGetKnownFolderPath
|
//sys shGetKnownFolderPath(id *KNOWNFOLDERID, flags uint32, token Token, path **uint16) (ret error) = shell32.SHGetKnownFolderPath
|
||||||
//sys TerminateProcess(handle Handle, exitcode uint32) (err error)
|
//sys TerminateProcess(handle Handle, exitcode uint32) (err error)
|
||||||
//sys GetExitCodeProcess(handle Handle, exitcode *uint32) (err error)
|
//sys GetExitCodeProcess(handle Handle, exitcode *uint32) (err error)
|
||||||
//sys GetStartupInfo(startupInfo *StartupInfo) (err error) = GetStartupInfoW
|
//sys getStartupInfo(startupInfo *StartupInfo) = GetStartupInfoW
|
||||||
//sys GetProcessTimes(handle Handle, creationTime *Filetime, exitTime *Filetime, kernelTime *Filetime, userTime *Filetime) (err error)
|
//sys GetProcessTimes(handle Handle, creationTime *Filetime, exitTime *Filetime, kernelTime *Filetime, userTime *Filetime) (err error)
|
||||||
//sys DuplicateHandle(hSourceProcessHandle Handle, hSourceHandle Handle, hTargetProcessHandle Handle, lpTargetHandle *Handle, dwDesiredAccess uint32, bInheritHandle bool, dwOptions uint32) (err error)
|
//sys DuplicateHandle(hSourceProcessHandle Handle, hSourceHandle Handle, hTargetProcessHandle Handle, lpTargetHandle *Handle, dwDesiredAccess uint32, bInheritHandle bool, dwOptions uint32) (err error)
|
||||||
//sys WaitForSingleObject(handle Handle, waitMilliseconds uint32) (event uint32, err error) [failretval==0xffffffff]
|
//sys WaitForSingleObject(handle Handle, waitMilliseconds uint32) (event uint32, err error) [failretval==0xffffffff]
|
||||||
@@ -235,12 +244,13 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
|||||||
//sys CreateEnvironmentBlock(block **uint16, token Token, inheritExisting bool) (err error) = userenv.CreateEnvironmentBlock
|
//sys CreateEnvironmentBlock(block **uint16, token Token, inheritExisting bool) (err error) = userenv.CreateEnvironmentBlock
|
||||||
//sys DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock
|
//sys DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock
|
||||||
//sys getTickCount64() (ms uint64) = kernel32.GetTickCount64
|
//sys getTickCount64() (ms uint64) = kernel32.GetTickCount64
|
||||||
|
//sys GetFileTime(handle Handle, ctime *Filetime, atime *Filetime, wtime *Filetime) (err error)
|
||||||
//sys SetFileTime(handle Handle, ctime *Filetime, atime *Filetime, wtime *Filetime) (err error)
|
//sys SetFileTime(handle Handle, ctime *Filetime, atime *Filetime, wtime *Filetime) (err error)
|
||||||
//sys GetFileAttributes(name *uint16) (attrs uint32, err error) [failretval==INVALID_FILE_ATTRIBUTES] = kernel32.GetFileAttributesW
|
//sys GetFileAttributes(name *uint16) (attrs uint32, err error) [failretval==INVALID_FILE_ATTRIBUTES] = kernel32.GetFileAttributesW
|
||||||
//sys SetFileAttributes(name *uint16, attrs uint32) (err error) = kernel32.SetFileAttributesW
|
//sys SetFileAttributes(name *uint16, attrs uint32) (err error) = kernel32.SetFileAttributesW
|
||||||
//sys GetFileAttributesEx(name *uint16, level uint32, info *byte) (err error) = kernel32.GetFileAttributesExW
|
//sys GetFileAttributesEx(name *uint16, level uint32, info *byte) (err error) = kernel32.GetFileAttributesExW
|
||||||
//sys GetCommandLine() (cmd *uint16) = kernel32.GetCommandLineW
|
//sys GetCommandLine() (cmd *uint16) = kernel32.GetCommandLineW
|
||||||
//sys CommandLineToArgv(cmd *uint16, argc *int32) (argv *[8192]*[8192]uint16, err error) [failretval==nil] = shell32.CommandLineToArgvW
|
//sys commandLineToArgv(cmd *uint16, argc *int32) (argv **uint16, err error) [failretval==nil] = shell32.CommandLineToArgvW
|
||||||
//sys LocalFree(hmem Handle) (handle Handle, err error) [failretval!=0]
|
//sys LocalFree(hmem Handle) (handle Handle, err error) [failretval!=0]
|
||||||
//sys LocalAlloc(flags uint32, length uint32) (ptr uintptr, err error)
|
//sys LocalAlloc(flags uint32, length uint32) (ptr uintptr, err error)
|
||||||
//sys SetHandleInformation(handle Handle, mask uint32, flags uint32) (err error)
|
//sys SetHandleInformation(handle Handle, mask uint32, flags uint32) (err error)
|
||||||
@@ -299,12 +309,19 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
|||||||
//sys RegNotifyChangeKeyValue(key Handle, watchSubtree bool, notifyFilter uint32, event Handle, asynchronous bool) (regerrno error) = advapi32.RegNotifyChangeKeyValue
|
//sys RegNotifyChangeKeyValue(key Handle, watchSubtree bool, notifyFilter uint32, event Handle, asynchronous bool) (regerrno error) = advapi32.RegNotifyChangeKeyValue
|
||||||
//sys GetCurrentProcessId() (pid uint32) = kernel32.GetCurrentProcessId
|
//sys GetCurrentProcessId() (pid uint32) = kernel32.GetCurrentProcessId
|
||||||
//sys ProcessIdToSessionId(pid uint32, sessionid *uint32) (err error) = kernel32.ProcessIdToSessionId
|
//sys ProcessIdToSessionId(pid uint32, sessionid *uint32) (err error) = kernel32.ProcessIdToSessionId
|
||||||
|
//sys ClosePseudoConsole(console Handle) = kernel32.ClosePseudoConsole
|
||||||
|
//sys createPseudoConsole(size uint32, in Handle, out Handle, flags uint32, pconsole *Handle) (hr error) = kernel32.CreatePseudoConsole
|
||||||
//sys GetConsoleMode(console Handle, mode *uint32) (err error) = kernel32.GetConsoleMode
|
//sys GetConsoleMode(console Handle, mode *uint32) (err error) = kernel32.GetConsoleMode
|
||||||
//sys SetConsoleMode(console Handle, mode uint32) (err error) = kernel32.SetConsoleMode
|
//sys SetConsoleMode(console Handle, mode uint32) (err error) = kernel32.SetConsoleMode
|
||||||
//sys GetConsoleScreenBufferInfo(console Handle, info *ConsoleScreenBufferInfo) (err error) = kernel32.GetConsoleScreenBufferInfo
|
//sys GetConsoleScreenBufferInfo(console Handle, info *ConsoleScreenBufferInfo) (err error) = kernel32.GetConsoleScreenBufferInfo
|
||||||
//sys setConsoleCursorPosition(console Handle, position uint32) (err error) = kernel32.SetConsoleCursorPosition
|
//sys setConsoleCursorPosition(console Handle, position uint32) (err error) = kernel32.SetConsoleCursorPosition
|
||||||
|
//sys GetConsoleCP() (cp uint32, err error) = kernel32.GetConsoleCP
|
||||||
|
//sys GetConsoleOutputCP() (cp uint32, err error) = kernel32.GetConsoleOutputCP
|
||||||
|
//sys SetConsoleCP(cp uint32) (err error) = kernel32.SetConsoleCP
|
||||||
|
//sys SetConsoleOutputCP(cp uint32) (err error) = kernel32.SetConsoleOutputCP
|
||||||
//sys WriteConsole(console Handle, buf *uint16, towrite uint32, written *uint32, reserved *byte) (err error) = kernel32.WriteConsoleW
|
//sys WriteConsole(console Handle, buf *uint16, towrite uint32, written *uint32, reserved *byte) (err error) = kernel32.WriteConsoleW
|
||||||
//sys ReadConsole(console Handle, buf *uint16, toread uint32, read *uint32, inputControl *byte) (err error) = kernel32.ReadConsoleW
|
//sys ReadConsole(console Handle, buf *uint16, toread uint32, read *uint32, inputControl *byte) (err error) = kernel32.ReadConsoleW
|
||||||
|
//sys resizePseudoConsole(pconsole Handle, size uint32) (hr error) = kernel32.ResizePseudoConsole
|
||||||
//sys CreateToolhelp32Snapshot(flags uint32, processId uint32) (handle Handle, err error) [failretval==InvalidHandle] = kernel32.CreateToolhelp32Snapshot
|
//sys CreateToolhelp32Snapshot(flags uint32, processId uint32) (handle Handle, err error) [failretval==InvalidHandle] = kernel32.CreateToolhelp32Snapshot
|
||||||
//sys Module32First(snapshot Handle, moduleEntry *ModuleEntry32) (err error) = kernel32.Module32FirstW
|
//sys Module32First(snapshot Handle, moduleEntry *ModuleEntry32) (err error) = kernel32.Module32FirstW
|
||||||
//sys Module32Next(snapshot Handle, moduleEntry *ModuleEntry32) (err error) = kernel32.Module32NextW
|
//sys Module32Next(snapshot Handle, moduleEntry *ModuleEntry32) (err error) = kernel32.Module32NextW
|
||||||
@@ -344,8 +361,19 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
|||||||
//sys SetProcessPriorityBoost(process Handle, disable bool) (err error) = kernel32.SetProcessPriorityBoost
|
//sys SetProcessPriorityBoost(process Handle, disable bool) (err error) = kernel32.SetProcessPriorityBoost
|
||||||
//sys GetProcessWorkingSetSizeEx(hProcess Handle, lpMinimumWorkingSetSize *uintptr, lpMaximumWorkingSetSize *uintptr, flags *uint32)
|
//sys GetProcessWorkingSetSizeEx(hProcess Handle, lpMinimumWorkingSetSize *uintptr, lpMaximumWorkingSetSize *uintptr, flags *uint32)
|
||||||
//sys SetProcessWorkingSetSizeEx(hProcess Handle, dwMinimumWorkingSetSize uintptr, dwMaximumWorkingSetSize uintptr, flags uint32) (err error)
|
//sys SetProcessWorkingSetSizeEx(hProcess Handle, dwMinimumWorkingSetSize uintptr, dwMaximumWorkingSetSize uintptr, flags uint32) (err error)
|
||||||
|
//sys ClearCommBreak(handle Handle) (err error)
|
||||||
|
//sys ClearCommError(handle Handle, lpErrors *uint32, lpStat *ComStat) (err error)
|
||||||
|
//sys EscapeCommFunction(handle Handle, dwFunc uint32) (err error)
|
||||||
|
//sys GetCommState(handle Handle, lpDCB *DCB) (err error)
|
||||||
|
//sys GetCommModemStatus(handle Handle, lpModemStat *uint32) (err error)
|
||||||
//sys GetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error)
|
//sys GetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error)
|
||||||
|
//sys PurgeComm(handle Handle, dwFlags uint32) (err error)
|
||||||
|
//sys SetCommBreak(handle Handle) (err error)
|
||||||
|
//sys SetCommMask(handle Handle, dwEvtMask uint32) (err error)
|
||||||
|
//sys SetCommState(handle Handle, lpDCB *DCB) (err error)
|
||||||
//sys SetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error)
|
//sys SetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error)
|
||||||
|
//sys SetupComm(handle Handle, dwInQueue uint32, dwOutQueue uint32) (err error)
|
||||||
|
//sys WaitCommEvent(handle Handle, lpEvtMask *uint32, lpOverlapped *Overlapped) (err error)
|
||||||
//sys GetActiveProcessorCount(groupNumber uint16) (ret uint32)
|
//sys GetActiveProcessorCount(groupNumber uint16) (ret uint32)
|
||||||
//sys GetMaximumProcessorCount(groupNumber uint16) (ret uint32)
|
//sys GetMaximumProcessorCount(groupNumber uint16) (ret uint32)
|
||||||
//sys EnumWindows(enumFunc uintptr, param unsafe.Pointer) (err error) = user32.EnumWindows
|
//sys EnumWindows(enumFunc uintptr, param unsafe.Pointer) (err error) = user32.EnumWindows
|
||||||
@@ -405,7 +433,7 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
|||||||
//sys VerQueryValue(block unsafe.Pointer, subBlock string, pointerToBufferPointer unsafe.Pointer, bufSize *uint32) (err error) = version.VerQueryValueW
|
//sys VerQueryValue(block unsafe.Pointer, subBlock string, pointerToBufferPointer unsafe.Pointer, bufSize *uint32) (err error) = version.VerQueryValueW
|
||||||
|
|
||||||
// Process Status API (PSAPI)
|
// Process Status API (PSAPI)
|
||||||
//sys EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
|
//sys enumProcesses(processIds *uint32, nSize uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
|
||||||
//sys EnumProcessModules(process Handle, module *Handle, cb uint32, cbNeeded *uint32) (err error) = psapi.EnumProcessModules
|
//sys EnumProcessModules(process Handle, module *Handle, cb uint32, cbNeeded *uint32) (err error) = psapi.EnumProcessModules
|
||||||
//sys EnumProcessModulesEx(process Handle, module *Handle, cb uint32, cbNeeded *uint32, filterFlag uint32) (err error) = psapi.EnumProcessModulesEx
|
//sys EnumProcessModulesEx(process Handle, module *Handle, cb uint32, cbNeeded *uint32, filterFlag uint32) (err error) = psapi.EnumProcessModulesEx
|
||||||
//sys GetModuleInformation(process Handle, module Handle, modinfo *ModuleInfo, cb uint32) (err error) = psapi.GetModuleInformation
|
//sys GetModuleInformation(process Handle, module Handle, modinfo *ModuleInfo, cb uint32) (err error) = psapi.GetModuleInformation
|
||||||
@@ -437,6 +465,10 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
|||||||
//sys DwmGetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, size uint32) (ret error) = dwmapi.DwmGetWindowAttribute
|
//sys DwmGetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, size uint32) (ret error) = dwmapi.DwmGetWindowAttribute
|
||||||
//sys DwmSetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, size uint32) (ret error) = dwmapi.DwmSetWindowAttribute
|
//sys DwmSetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, size uint32) (ret error) = dwmapi.DwmSetWindowAttribute
|
||||||
|
|
||||||
|
// Windows Multimedia API
|
||||||
|
//sys TimeBeginPeriod (period uint32) (err error) [failretval != 0] = winmm.timeBeginPeriod
|
||||||
|
//sys TimeEndPeriod (period uint32) (err error) [failretval != 0] = winmm.timeEndPeriod
|
||||||
|
|
||||||
// syscall interface implementation for other packages
|
// syscall interface implementation for other packages
|
||||||
|
|
||||||
// GetCurrentProcess returns the handle for the current process.
|
// GetCurrentProcess returns the handle for the current process.
|
||||||
@@ -695,20 +727,12 @@ func DurationSinceBoot() time.Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Ftruncate(fd Handle, length int64) (err error) {
|
func Ftruncate(fd Handle, length int64) (err error) {
|
||||||
curoffset, e := Seek(fd, 0, 1)
|
type _FILE_END_OF_FILE_INFO struct {
|
||||||
if e != nil {
|
EndOfFile int64
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
defer Seek(fd, curoffset, 0)
|
var info _FILE_END_OF_FILE_INFO
|
||||||
_, e = Seek(fd, length, 0)
|
info.EndOfFile = length
|
||||||
if e != nil {
|
return SetFileInformationByHandle(fd, FileEndOfFileInfo, (*byte)(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info)))
|
||||||
return e
|
|
||||||
}
|
|
||||||
e = SetEndOfFile(fd)
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Gettimeofday(tv *Timeval) (err error) {
|
func Gettimeofday(tv *Timeval) (err error) {
|
||||||
@@ -864,6 +888,11 @@ const socket_error = uintptr(^uint32(0))
|
|||||||
//sys GetACP() (acp uint32) = kernel32.GetACP
|
//sys GetACP() (acp uint32) = kernel32.GetACP
|
||||||
//sys MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32, wchar *uint16, nwchar int32) (nwrite int32, err error) = kernel32.MultiByteToWideChar
|
//sys MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32, wchar *uint16, nwchar int32) (nwrite int32, err error) = kernel32.MultiByteToWideChar
|
||||||
//sys getBestInterfaceEx(sockaddr unsafe.Pointer, pdwBestIfIndex *uint32) (errcode error) = iphlpapi.GetBestInterfaceEx
|
//sys getBestInterfaceEx(sockaddr unsafe.Pointer, pdwBestIfIndex *uint32) (errcode error) = iphlpapi.GetBestInterfaceEx
|
||||||
|
//sys GetIfEntry2Ex(level uint32, row *MibIfRow2) (errcode error) = iphlpapi.GetIfEntry2Ex
|
||||||
|
//sys GetUnicastIpAddressEntry(row *MibUnicastIpAddressRow) (errcode error) = iphlpapi.GetUnicastIpAddressEntry
|
||||||
|
//sys NotifyIpInterfaceChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyIpInterfaceChange
|
||||||
|
//sys NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyUnicastIpAddressChange
|
||||||
|
//sys CancelMibChangeNotify2(notificationHandle Handle) (errcode error) = iphlpapi.CancelMibChangeNotify2
|
||||||
|
|
||||||
// For testing: clients can set this flag to force
|
// For testing: clients can set this flag to force
|
||||||
// creation of IPv6 sockets to return EAFNOSUPPORT.
|
// creation of IPv6 sockets to return EAFNOSUPPORT.
|
||||||
@@ -964,7 +993,8 @@ func (sa *SockaddrUnix) sockaddr() (unsafe.Pointer, int32, error) {
|
|||||||
if n > 0 {
|
if n > 0 {
|
||||||
sl += int32(n) + 1
|
sl += int32(n) + 1
|
||||||
}
|
}
|
||||||
if sa.raw.Path[0] == '@' {
|
if sa.raw.Path[0] == '@' || (sa.raw.Path[0] == 0 && sl > 3) {
|
||||||
|
// Check sl > 3 so we don't change unnamed socket behavior.
|
||||||
sa.raw.Path[0] = 0
|
sa.raw.Path[0] = 0
|
||||||
// Don't count trailing NUL for abstract address.
|
// Don't count trailing NUL for abstract address.
|
||||||
sl--
|
sl--
|
||||||
@@ -1347,13 +1377,26 @@ func SetsockoptLinger(fd Handle, level, opt int, l *Linger) (err error) {
|
|||||||
func SetsockoptInet4Addr(fd Handle, level, opt int, value [4]byte) (err error) {
|
func SetsockoptInet4Addr(fd Handle, level, opt int, value [4]byte) (err error) {
|
||||||
return Setsockopt(fd, int32(level), int32(opt), (*byte)(unsafe.Pointer(&value[0])), 4)
|
return Setsockopt(fd, int32(level), int32(opt), (*byte)(unsafe.Pointer(&value[0])), 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetsockoptIPMreq(fd Handle, level, opt int, mreq *IPMreq) (err error) {
|
func SetsockoptIPMreq(fd Handle, level, opt int, mreq *IPMreq) (err error) {
|
||||||
return Setsockopt(fd, int32(level), int32(opt), (*byte)(unsafe.Pointer(mreq)), int32(unsafe.Sizeof(*mreq)))
|
return Setsockopt(fd, int32(level), int32(opt), (*byte)(unsafe.Pointer(mreq)), int32(unsafe.Sizeof(*mreq)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetsockoptIPv6Mreq(fd Handle, level, opt int, mreq *IPv6Mreq) (err error) {
|
func SetsockoptIPv6Mreq(fd Handle, level, opt int, mreq *IPv6Mreq) (err error) {
|
||||||
return syscall.EWINDOWS
|
return syscall.EWINDOWS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EnumProcesses(processIds []uint32, bytesReturned *uint32) error {
|
||||||
|
// EnumProcesses syscall expects the size parameter to be in bytes, but the code generated with mksyscall uses
|
||||||
|
// the length of the processIds slice instead. Hence, this wrapper function is added to fix the discrepancy.
|
||||||
|
var p *uint32
|
||||||
|
if len(processIds) > 0 {
|
||||||
|
p = &processIds[0]
|
||||||
|
}
|
||||||
|
size := uint32(len(processIds) * 4)
|
||||||
|
return enumProcesses(p, size, bytesReturned)
|
||||||
|
}
|
||||||
|
|
||||||
func Getpid() (pid int) { return int(GetCurrentProcessId()) }
|
func Getpid() (pid int) { return int(GetCurrentProcessId()) }
|
||||||
|
|
||||||
func FindFirstFile(name *uint16, data *Win32finddata) (handle Handle, err error) {
|
func FindFirstFile(name *uint16, data *Win32finddata) (handle Handle, err error) {
|
||||||
@@ -1613,6 +1656,11 @@ func SetConsoleCursorPosition(console Handle, position Coord) error {
|
|||||||
return setConsoleCursorPosition(console, *((*uint32)(unsafe.Pointer(&position))))
|
return setConsoleCursorPosition(console, *((*uint32)(unsafe.Pointer(&position))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetStartupInfo(startupInfo *StartupInfo) error {
|
||||||
|
getStartupInfo(startupInfo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s NTStatus) Errno() syscall.Errno {
|
func (s NTStatus) Errno() syscall.Errno {
|
||||||
return rtlNtStatusToDosErrorNoTeb(s)
|
return rtlNtStatusToDosErrorNoTeb(s)
|
||||||
}
|
}
|
||||||
@@ -1636,23 +1684,22 @@ func (s NTStatus) Error() string {
|
|||||||
// do not use NTUnicodeString, and instead UTF16PtrFromString should be used for
|
// do not use NTUnicodeString, and instead UTF16PtrFromString should be used for
|
||||||
// the more common *uint16 string type.
|
// the more common *uint16 string type.
|
||||||
func NewNTUnicodeString(s string) (*NTUnicodeString, error) {
|
func NewNTUnicodeString(s string) (*NTUnicodeString, error) {
|
||||||
var u NTUnicodeString
|
s16, err := UTF16FromString(s)
|
||||||
s16, err := UTF16PtrFromString(s)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
RtlInitUnicodeString(&u, s16)
|
n := uint16(len(s16) * 2)
|
||||||
return &u, nil
|
return &NTUnicodeString{
|
||||||
|
Length: n - 2, // subtract 2 bytes for the NULL terminator
|
||||||
|
MaximumLength: n,
|
||||||
|
Buffer: &s16[0],
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slice returns a uint16 slice that aliases the data in the NTUnicodeString.
|
// Slice returns a uint16 slice that aliases the data in the NTUnicodeString.
|
||||||
func (s *NTUnicodeString) Slice() []uint16 {
|
func (s *NTUnicodeString) Slice() []uint16 {
|
||||||
var slice []uint16
|
slice := unsafe.Slice(s.Buffer, s.MaximumLength)
|
||||||
hdr := (*unsafeheader.Slice)(unsafe.Pointer(&slice))
|
return slice[:s.Length]
|
||||||
hdr.Data = unsafe.Pointer(s.Buffer)
|
|
||||||
hdr.Len = int(s.Length)
|
|
||||||
hdr.Cap = int(s.MaximumLength)
|
|
||||||
return slice
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NTUnicodeString) String() string {
|
func (s *NTUnicodeString) String() string {
|
||||||
@@ -1675,12 +1722,8 @@ func NewNTString(s string) (*NTString, error) {
|
|||||||
|
|
||||||
// Slice returns a byte slice that aliases the data in the NTString.
|
// Slice returns a byte slice that aliases the data in the NTString.
|
||||||
func (s *NTString) Slice() []byte {
|
func (s *NTString) Slice() []byte {
|
||||||
var slice []byte
|
slice := unsafe.Slice(s.Buffer, s.MaximumLength)
|
||||||
hdr := (*unsafeheader.Slice)(unsafe.Pointer(&slice))
|
return slice[:s.Length]
|
||||||
hdr.Data = unsafe.Pointer(s.Buffer)
|
|
||||||
hdr.Len = int(s.Length)
|
|
||||||
hdr.Cap = int(s.MaximumLength)
|
|
||||||
return slice
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NTString) String() string {
|
func (s *NTString) String() string {
|
||||||
@@ -1732,10 +1775,7 @@ func LoadResourceData(module, resInfo Handle) (data []byte, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h := (*unsafeheader.Slice)(unsafe.Pointer(&data))
|
data = unsafe.Slice((*byte)(unsafe.Pointer(ptr)), size)
|
||||||
h.Data = unsafe.Pointer(ptr)
|
|
||||||
h.Len = int(size)
|
|
||||||
h.Cap = int(size)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1806,3 +1846,87 @@ type PSAPI_WORKING_SET_EX_INFORMATION struct {
|
|||||||
// A PSAPI_WORKING_SET_EX_BLOCK union that indicates the attributes of the page at VirtualAddress.
|
// A PSAPI_WORKING_SET_EX_BLOCK union that indicates the attributes of the page at VirtualAddress.
|
||||||
VirtualAttributes PSAPI_WORKING_SET_EX_BLOCK
|
VirtualAttributes PSAPI_WORKING_SET_EX_BLOCK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreatePseudoConsole creates a windows pseudo console.
|
||||||
|
func CreatePseudoConsole(size Coord, in Handle, out Handle, flags uint32, pconsole *Handle) error {
|
||||||
|
// We need this wrapper to manually cast Coord to uint32. The autogenerated wrappers only
|
||||||
|
// accept arguments that can be casted to uintptr, and Coord can't.
|
||||||
|
return createPseudoConsole(*((*uint32)(unsafe.Pointer(&size))), in, out, flags, pconsole)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizePseudoConsole resizes the internal buffers of the pseudo console to the width and height specified in `size`.
|
||||||
|
func ResizePseudoConsole(pconsole Handle, size Coord) error {
|
||||||
|
// We need this wrapper to manually cast Coord to uint32. The autogenerated wrappers only
|
||||||
|
// accept arguments that can be casted to uintptr, and Coord can't.
|
||||||
|
return resizePseudoConsole(pconsole, *((*uint32)(unsafe.Pointer(&size))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DCB constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-dcb.
|
||||||
|
const (
|
||||||
|
CBR_110 = 110
|
||||||
|
CBR_300 = 300
|
||||||
|
CBR_600 = 600
|
||||||
|
CBR_1200 = 1200
|
||||||
|
CBR_2400 = 2400
|
||||||
|
CBR_4800 = 4800
|
||||||
|
CBR_9600 = 9600
|
||||||
|
CBR_14400 = 14400
|
||||||
|
CBR_19200 = 19200
|
||||||
|
CBR_38400 = 38400
|
||||||
|
CBR_57600 = 57600
|
||||||
|
CBR_115200 = 115200
|
||||||
|
CBR_128000 = 128000
|
||||||
|
CBR_256000 = 256000
|
||||||
|
|
||||||
|
DTR_CONTROL_DISABLE = 0x00000000
|
||||||
|
DTR_CONTROL_ENABLE = 0x00000010
|
||||||
|
DTR_CONTROL_HANDSHAKE = 0x00000020
|
||||||
|
|
||||||
|
RTS_CONTROL_DISABLE = 0x00000000
|
||||||
|
RTS_CONTROL_ENABLE = 0x00001000
|
||||||
|
RTS_CONTROL_HANDSHAKE = 0x00002000
|
||||||
|
RTS_CONTROL_TOGGLE = 0x00003000
|
||||||
|
|
||||||
|
NOPARITY = 0
|
||||||
|
ODDPARITY = 1
|
||||||
|
EVENPARITY = 2
|
||||||
|
MARKPARITY = 3
|
||||||
|
SPACEPARITY = 4
|
||||||
|
|
||||||
|
ONESTOPBIT = 0
|
||||||
|
ONE5STOPBITS = 1
|
||||||
|
TWOSTOPBITS = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// EscapeCommFunction constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-escapecommfunction.
|
||||||
|
const (
|
||||||
|
SETXOFF = 1
|
||||||
|
SETXON = 2
|
||||||
|
SETRTS = 3
|
||||||
|
CLRRTS = 4
|
||||||
|
SETDTR = 5
|
||||||
|
CLRDTR = 6
|
||||||
|
SETBREAK = 8
|
||||||
|
CLRBREAK = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
// PurgeComm constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-purgecomm.
|
||||||
|
const (
|
||||||
|
PURGE_TXABORT = 0x0001
|
||||||
|
PURGE_RXABORT = 0x0002
|
||||||
|
PURGE_TXCLEAR = 0x0004
|
||||||
|
PURGE_RXCLEAR = 0x0008
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetCommMask constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setcommmask.
|
||||||
|
const (
|
||||||
|
EV_RXCHAR = 0x0001
|
||||||
|
EV_RXFLAG = 0x0002
|
||||||
|
EV_TXEMPTY = 0x0004
|
||||||
|
EV_CTS = 0x0008
|
||||||
|
EV_DSR = 0x0010
|
||||||
|
EV_RLSD = 0x0020
|
||||||
|
EV_BREAK = 0x0040
|
||||||
|
EV_ERR = 0x0080
|
||||||
|
EV_RING = 0x0100
|
||||||
|
)
|
||||||
|
|||||||
268
vendor/golang.org/x/sys/windows/types_windows.go
generated
vendored
268
vendor/golang.org/x/sys/windows/types_windows.go
generated
vendored
@@ -176,6 +176,7 @@ const (
|
|||||||
WAIT_FAILED = 0xFFFFFFFF
|
WAIT_FAILED = 0xFFFFFFFF
|
||||||
|
|
||||||
// Access rights for process.
|
// Access rights for process.
|
||||||
|
PROCESS_ALL_ACCESS = 0xFFFF
|
||||||
PROCESS_CREATE_PROCESS = 0x0080
|
PROCESS_CREATE_PROCESS = 0x0080
|
||||||
PROCESS_CREATE_THREAD = 0x0002
|
PROCESS_CREATE_THREAD = 0x0002
|
||||||
PROCESS_DUP_HANDLE = 0x0040
|
PROCESS_DUP_HANDLE = 0x0040
|
||||||
@@ -247,6 +248,7 @@ const (
|
|||||||
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007
|
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007
|
||||||
PROC_THREAD_ATTRIBUTE_UMS_THREAD = 0x00030006
|
PROC_THREAD_ATTRIBUTE_UMS_THREAD = 0x00030006
|
||||||
PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL = 0x0002000b
|
PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL = 0x0002000b
|
||||||
|
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -1059,6 +1061,7 @@ const (
|
|||||||
SIO_GET_EXTENSION_FUNCTION_POINTER = IOC_INOUT | IOC_WS2 | 6
|
SIO_GET_EXTENSION_FUNCTION_POINTER = IOC_INOUT | IOC_WS2 | 6
|
||||||
SIO_KEEPALIVE_VALS = IOC_IN | IOC_VENDOR | 4
|
SIO_KEEPALIVE_VALS = IOC_IN | IOC_VENDOR | 4
|
||||||
SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12
|
SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12
|
||||||
|
SIO_UDP_NETRESET = IOC_IN | IOC_VENDOR | 15
|
||||||
|
|
||||||
// cf. http://support.microsoft.com/default.aspx?scid=kb;en-us;257460
|
// cf. http://support.microsoft.com/default.aspx?scid=kb;en-us;257460
|
||||||
|
|
||||||
@@ -1093,7 +1096,33 @@ const (
|
|||||||
|
|
||||||
SOMAXCONN = 0x7fffffff
|
SOMAXCONN = 0x7fffffff
|
||||||
|
|
||||||
TCP_NODELAY = 1
|
TCP_NODELAY = 1
|
||||||
|
TCP_EXPEDITED_1122 = 2
|
||||||
|
TCP_KEEPALIVE = 3
|
||||||
|
TCP_MAXSEG = 4
|
||||||
|
TCP_MAXRT = 5
|
||||||
|
TCP_STDURG = 6
|
||||||
|
TCP_NOURG = 7
|
||||||
|
TCP_ATMARK = 8
|
||||||
|
TCP_NOSYNRETRIES = 9
|
||||||
|
TCP_TIMESTAMPS = 10
|
||||||
|
TCP_OFFLOAD_PREFERENCE = 11
|
||||||
|
TCP_CONGESTION_ALGORITHM = 12
|
||||||
|
TCP_DELAY_FIN_ACK = 13
|
||||||
|
TCP_MAXRTMS = 14
|
||||||
|
TCP_FASTOPEN = 15
|
||||||
|
TCP_KEEPCNT = 16
|
||||||
|
TCP_KEEPIDLE = TCP_KEEPALIVE
|
||||||
|
TCP_KEEPINTVL = 17
|
||||||
|
TCP_FAIL_CONNECT_ON_ICMP_ERROR = 18
|
||||||
|
TCP_ICMP_ERROR_INFO = 19
|
||||||
|
|
||||||
|
UDP_NOCHECKSUM = 1
|
||||||
|
UDP_SEND_MSG_SIZE = 2
|
||||||
|
UDP_RECV_MAX_COALESCED_SIZE = 3
|
||||||
|
UDP_CHECKSUM_COVERAGE = 20
|
||||||
|
|
||||||
|
UDP_COALESCED_INFO = 3
|
||||||
|
|
||||||
SHUT_RD = 0
|
SHUT_RD = 0
|
||||||
SHUT_WR = 1
|
SHUT_WR = 1
|
||||||
@@ -1976,7 +2005,21 @@ const (
|
|||||||
MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x20
|
MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x20
|
||||||
)
|
)
|
||||||
|
|
||||||
const GAA_FLAG_INCLUDE_PREFIX = 0x00000010
|
// Flags for GetAdaptersAddresses, see
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses.
|
||||||
|
const (
|
||||||
|
GAA_FLAG_SKIP_UNICAST = 0x1
|
||||||
|
GAA_FLAG_SKIP_ANYCAST = 0x2
|
||||||
|
GAA_FLAG_SKIP_MULTICAST = 0x4
|
||||||
|
GAA_FLAG_SKIP_DNS_SERVER = 0x8
|
||||||
|
GAA_FLAG_INCLUDE_PREFIX = 0x10
|
||||||
|
GAA_FLAG_SKIP_FRIENDLY_NAME = 0x20
|
||||||
|
GAA_FLAG_INCLUDE_WINS_INFO = 0x40
|
||||||
|
GAA_FLAG_INCLUDE_GATEWAYS = 0x80
|
||||||
|
GAA_FLAG_INCLUDE_ALL_INTERFACES = 0x100
|
||||||
|
GAA_FLAG_INCLUDE_ALL_COMPARTMENTS = 0x200
|
||||||
|
GAA_FLAG_INCLUDE_TUNNEL_BINDINGORDER = 0x400
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
IF_TYPE_OTHER = 1
|
IF_TYPE_OTHER = 1
|
||||||
@@ -1990,6 +2033,50 @@ const (
|
|||||||
IF_TYPE_IEEE1394 = 144
|
IF_TYPE_IEEE1394 = 144
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Enum NL_PREFIX_ORIGIN for [IpAdapterUnicastAddress], see
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/nldef/ne-nldef-nl_prefix_origin
|
||||||
|
const (
|
||||||
|
IpPrefixOriginOther = 0
|
||||||
|
IpPrefixOriginManual = 1
|
||||||
|
IpPrefixOriginWellKnown = 2
|
||||||
|
IpPrefixOriginDhcp = 3
|
||||||
|
IpPrefixOriginRouterAdvertisement = 4
|
||||||
|
IpPrefixOriginUnchanged = 1 << 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum NL_SUFFIX_ORIGIN for [IpAdapterUnicastAddress], see
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/nldef/ne-nldef-nl_suffix_origin
|
||||||
|
const (
|
||||||
|
NlsoOther = 0
|
||||||
|
NlsoManual = 1
|
||||||
|
NlsoWellKnown = 2
|
||||||
|
NlsoDhcp = 3
|
||||||
|
NlsoLinkLayerAddress = 4
|
||||||
|
NlsoRandom = 5
|
||||||
|
IpSuffixOriginOther = 0
|
||||||
|
IpSuffixOriginManual = 1
|
||||||
|
IpSuffixOriginWellKnown = 2
|
||||||
|
IpSuffixOriginDhcp = 3
|
||||||
|
IpSuffixOriginLinkLayerAddress = 4
|
||||||
|
IpSuffixOriginRandom = 5
|
||||||
|
IpSuffixOriginUnchanged = 1 << 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum NL_DAD_STATE for [IpAdapterUnicastAddress], see
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/nldef/ne-nldef-nl_dad_state
|
||||||
|
const (
|
||||||
|
NldsInvalid = 0
|
||||||
|
NldsTentative = 1
|
||||||
|
NldsDuplicate = 2
|
||||||
|
NldsDeprecated = 3
|
||||||
|
NldsPreferred = 4
|
||||||
|
IpDadStateInvalid = 0
|
||||||
|
IpDadStateTentative = 1
|
||||||
|
IpDadStateDuplicate = 2
|
||||||
|
IpDadStateDeprecated = 3
|
||||||
|
IpDadStatePreferred = 4
|
||||||
|
)
|
||||||
|
|
||||||
type SocketAddress struct {
|
type SocketAddress struct {
|
||||||
Sockaddr *syscall.RawSockaddrAny
|
Sockaddr *syscall.RawSockaddrAny
|
||||||
SockaddrLength int32
|
SockaddrLength int32
|
||||||
@@ -2117,6 +2204,132 @@ const (
|
|||||||
IfOperStatusLowerLayerDown = 7
|
IfOperStatusLowerLayerDown = 7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IF_MAX_PHYS_ADDRESS_LENGTH = 32
|
||||||
|
IF_MAX_STRING_SIZE = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
// MIB_IF_ENTRY_LEVEL enumeration from netioapi.h or
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getifentry2ex.
|
||||||
|
const (
|
||||||
|
MibIfEntryNormal = 0
|
||||||
|
MibIfEntryNormalWithoutStatistics = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// MIB_NOTIFICATION_TYPE enumeration from netioapi.h or
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ne-netioapi-mib_notification_type.
|
||||||
|
const (
|
||||||
|
MibParameterNotification = 0
|
||||||
|
MibAddInstance = 1
|
||||||
|
MibDeleteInstance = 2
|
||||||
|
MibInitialNotification = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// MibIfRow2 stores information about a particular interface. See
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_if_row2.
|
||||||
|
type MibIfRow2 struct {
|
||||||
|
InterfaceLuid uint64
|
||||||
|
InterfaceIndex uint32
|
||||||
|
InterfaceGuid GUID
|
||||||
|
Alias [IF_MAX_STRING_SIZE + 1]uint16
|
||||||
|
Description [IF_MAX_STRING_SIZE + 1]uint16
|
||||||
|
PhysicalAddressLength uint32
|
||||||
|
PhysicalAddress [IF_MAX_PHYS_ADDRESS_LENGTH]uint8
|
||||||
|
PermanentPhysicalAddress [IF_MAX_PHYS_ADDRESS_LENGTH]uint8
|
||||||
|
Mtu uint32
|
||||||
|
Type uint32
|
||||||
|
TunnelType uint32
|
||||||
|
MediaType uint32
|
||||||
|
PhysicalMediumType uint32
|
||||||
|
AccessType uint32
|
||||||
|
DirectionType uint32
|
||||||
|
InterfaceAndOperStatusFlags uint8
|
||||||
|
OperStatus uint32
|
||||||
|
AdminStatus uint32
|
||||||
|
MediaConnectState uint32
|
||||||
|
NetworkGuid GUID
|
||||||
|
ConnectionType uint32
|
||||||
|
TransmitLinkSpeed uint64
|
||||||
|
ReceiveLinkSpeed uint64
|
||||||
|
InOctets uint64
|
||||||
|
InUcastPkts uint64
|
||||||
|
InNUcastPkts uint64
|
||||||
|
InDiscards uint64
|
||||||
|
InErrors uint64
|
||||||
|
InUnknownProtos uint64
|
||||||
|
InUcastOctets uint64
|
||||||
|
InMulticastOctets uint64
|
||||||
|
InBroadcastOctets uint64
|
||||||
|
OutOctets uint64
|
||||||
|
OutUcastPkts uint64
|
||||||
|
OutNUcastPkts uint64
|
||||||
|
OutDiscards uint64
|
||||||
|
OutErrors uint64
|
||||||
|
OutUcastOctets uint64
|
||||||
|
OutMulticastOctets uint64
|
||||||
|
OutBroadcastOctets uint64
|
||||||
|
OutQLen uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIB_UNICASTIPADDRESS_ROW stores information about a unicast IP address. See
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_unicastipaddress_row.
|
||||||
|
type MibUnicastIpAddressRow struct {
|
||||||
|
Address RawSockaddrInet6 // SOCKADDR_INET union
|
||||||
|
InterfaceLuid uint64
|
||||||
|
InterfaceIndex uint32
|
||||||
|
PrefixOrigin uint32
|
||||||
|
SuffixOrigin uint32
|
||||||
|
ValidLifetime uint32
|
||||||
|
PreferredLifetime uint32
|
||||||
|
OnLinkPrefixLength uint8
|
||||||
|
SkipAsSource uint8
|
||||||
|
DadState uint32
|
||||||
|
ScopeId uint32
|
||||||
|
CreationTimeStamp Filetime
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScopeLevelCount = 16
|
||||||
|
|
||||||
|
// MIB_IPINTERFACE_ROW stores interface management information for a particular IP address family on a network interface.
|
||||||
|
// See https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipinterface_row.
|
||||||
|
type MibIpInterfaceRow struct {
|
||||||
|
Family uint16
|
||||||
|
InterfaceLuid uint64
|
||||||
|
InterfaceIndex uint32
|
||||||
|
MaxReassemblySize uint32
|
||||||
|
InterfaceIdentifier uint64
|
||||||
|
MinRouterAdvertisementInterval uint32
|
||||||
|
MaxRouterAdvertisementInterval uint32
|
||||||
|
AdvertisingEnabled uint8
|
||||||
|
ForwardingEnabled uint8
|
||||||
|
WeakHostSend uint8
|
||||||
|
WeakHostReceive uint8
|
||||||
|
UseAutomaticMetric uint8
|
||||||
|
UseNeighborUnreachabilityDetection uint8
|
||||||
|
ManagedAddressConfigurationSupported uint8
|
||||||
|
OtherStatefulConfigurationSupported uint8
|
||||||
|
AdvertiseDefaultRoute uint8
|
||||||
|
RouterDiscoveryBehavior uint32
|
||||||
|
DadTransmits uint32
|
||||||
|
BaseReachableTime uint32
|
||||||
|
RetransmitTime uint32
|
||||||
|
PathMtuDiscoveryTimeout uint32
|
||||||
|
LinkLocalAddressBehavior uint32
|
||||||
|
LinkLocalAddressTimeout uint32
|
||||||
|
ZoneIndices [ScopeLevelCount]uint32
|
||||||
|
SitePrefixLength uint32
|
||||||
|
Metric uint32
|
||||||
|
NlMtu uint32
|
||||||
|
Connected uint8
|
||||||
|
SupportsWakeUpPatterns uint8
|
||||||
|
SupportsNeighborDiscovery uint8
|
||||||
|
SupportsRouterDiscovery uint8
|
||||||
|
ReachableTime uint32
|
||||||
|
TransmitOffload uint32
|
||||||
|
ReceiveOffload uint32
|
||||||
|
DisableDefaultRoutes uint8
|
||||||
|
}
|
||||||
|
|
||||||
// Console related constants used for the mode parameter to SetConsoleMode. See
|
// Console related constants used for the mode parameter to SetConsoleMode. See
|
||||||
// https://docs.microsoft.com/en-us/windows/console/setconsolemode for details.
|
// https://docs.microsoft.com/en-us/windows/console/setconsolemode for details.
|
||||||
|
|
||||||
@@ -2139,6 +2352,12 @@ const (
|
|||||||
ENABLE_LVB_GRID_WORLDWIDE = 0x10
|
ENABLE_LVB_GRID_WORLDWIDE = 0x10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Pseudo console related constants used for the flags parameter to
|
||||||
|
// CreatePseudoConsole. See: https://learn.microsoft.com/en-us/windows/console/createpseudoconsole
|
||||||
|
const (
|
||||||
|
PSEUDOCONSOLE_INHERIT_CURSOR = 0x1
|
||||||
|
)
|
||||||
|
|
||||||
type Coord struct {
|
type Coord struct {
|
||||||
X int16
|
X int16
|
||||||
Y int16
|
Y int16
|
||||||
@@ -2220,19 +2439,23 @@ type JOBOBJECT_BASIC_UI_RESTRICTIONS struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// JobObjectInformationClass
|
// JobObjectInformationClass for QueryInformationJobObject and SetInformationJobObject
|
||||||
JobObjectAssociateCompletionPortInformation = 7
|
JobObjectAssociateCompletionPortInformation = 7
|
||||||
|
JobObjectBasicAccountingInformation = 1
|
||||||
|
JobObjectBasicAndIoAccountingInformation = 8
|
||||||
JobObjectBasicLimitInformation = 2
|
JobObjectBasicLimitInformation = 2
|
||||||
|
JobObjectBasicProcessIdList = 3
|
||||||
JobObjectBasicUIRestrictions = 4
|
JobObjectBasicUIRestrictions = 4
|
||||||
JobObjectCpuRateControlInformation = 15
|
JobObjectCpuRateControlInformation = 15
|
||||||
JobObjectEndOfJobTimeInformation = 6
|
JobObjectEndOfJobTimeInformation = 6
|
||||||
JobObjectExtendedLimitInformation = 9
|
JobObjectExtendedLimitInformation = 9
|
||||||
JobObjectGroupInformation = 11
|
JobObjectGroupInformation = 11
|
||||||
JobObjectGroupInformationEx = 14
|
JobObjectGroupInformationEx = 14
|
||||||
JobObjectLimitViolationInformation2 = 35
|
JobObjectLimitViolationInformation = 13
|
||||||
|
JobObjectLimitViolationInformation2 = 34
|
||||||
JobObjectNetRateControlInformation = 32
|
JobObjectNetRateControlInformation = 32
|
||||||
JobObjectNotificationLimitInformation = 12
|
JobObjectNotificationLimitInformation = 12
|
||||||
JobObjectNotificationLimitInformation2 = 34
|
JobObjectNotificationLimitInformation2 = 33
|
||||||
JobObjectSecurityLimitInformation = 5
|
JobObjectSecurityLimitInformation = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3343,3 +3566,38 @@ type BLOB struct {
|
|||||||
Size uint32
|
Size uint32
|
||||||
BlobData *byte
|
BlobData *byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ComStat struct {
|
||||||
|
Flags uint32
|
||||||
|
CBInQue uint32
|
||||||
|
CBOutQue uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type DCB struct {
|
||||||
|
DCBlength uint32
|
||||||
|
BaudRate uint32
|
||||||
|
Flags uint32
|
||||||
|
wReserved uint16
|
||||||
|
XonLim uint16
|
||||||
|
XoffLim uint16
|
||||||
|
ByteSize uint8
|
||||||
|
Parity uint8
|
||||||
|
StopBits uint8
|
||||||
|
XonChar byte
|
||||||
|
XoffChar byte
|
||||||
|
ErrorChar byte
|
||||||
|
EofChar byte
|
||||||
|
EvtChar byte
|
||||||
|
wReserved1 uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard Layout Flags.
|
||||||
|
// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadkeyboardlayoutw
|
||||||
|
const (
|
||||||
|
KLF_ACTIVATE = 0x00000001
|
||||||
|
KLF_SUBSTITUTE_OK = 0x00000002
|
||||||
|
KLF_REORDER = 0x00000008
|
||||||
|
KLF_REPLACELANG = 0x00000010
|
||||||
|
KLF_NOTELLSHELL = 0x00000080
|
||||||
|
KLF_SETFORPROCESS = 0x00000100
|
||||||
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user