Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91324e8de7 | ||
|
|
d63afc2c6d | ||
|
|
315e39d61e | ||
|
|
b1077531bf | ||
|
|
9ecc937dc4 | ||
|
|
7202b68733 | ||
|
|
6c5b7a3f4c | ||
|
|
36d5ae023f | ||
|
|
478c08f6a7 | ||
|
|
cae90d16ba | ||
|
|
fbc07bd142 | ||
|
|
4a4d3e1755 | ||
|
|
f243a4938f | ||
|
|
9416de1497 | ||
|
|
283a5fcfd0 |
@@ -20,6 +20,9 @@ builds:
|
|||||||
- 386
|
- 386
|
||||||
goarm:
|
goarm:
|
||||||
- 7
|
- 7
|
||||||
|
ignore:
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/glanceapp/glance/internal/glance.buildVersion={{ .Tag }}
|
- -s -w -X github.com/glanceapp/glance/internal/glance.buildVersion={{ .Tag }}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
FROM golang:1.24.3-alpine3.21 AS builder
|
FROM golang:1.26.3-alpine3.22 AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
RUN CGO_ENABLED=0 go build .
|
RUN CGO_ENABLED=0 go build .
|
||||||
|
|
||||||
FROM alpine:3.21
|
FROM alpine:3.22
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/glance .
|
COPY --from=builder /app/glance .
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.22
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY glance .
|
COPY glance .
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
<p align="center"><em>What if you could see everything at a...</em></p>
|
<p align="center"><img src="docs/logo.png"></p>
|
||||||
<h1 align="center">Glance</h1>
|
<h1 align="center">Glance</h1>
|
||||||
<p align="center"><a href="#installation">Install</a> • <a href="docs/configuration.md#configuring-glance">Configuration</a> • <a href="https://discord.com/invite/7KQ7Xa9kJd">Discord</a> • <a href="https://github.com/sponsors/glanceapp">Sponsor</a></p>
|
<p align="center">
|
||||||
<p align="center"><a href="https://github.com/glanceapp/community-widgets">Community widgets</a> • <a href="docs/preconfigured-pages.md">Preconfigured pages</a> • <a href="docs/themes.md">Themes</a></p>
|
<a href="#installation">Install</a> •
|
||||||
|
<a href="docs/configuration.md#configuring-glance">Configuration</a> •
|
||||||
|
<a href="https://discord.com/invite/7KQ7Xa9kJd">Discord</a> •
|
||||||
|
<a href="https://github.com/sponsors/glanceapp">Sponsor</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/glanceapp/community-widgets">Community widgets</a> •
|
||||||
|
<a href="docs/preconfigured-pages.md">Preconfigured pages</a> •
|
||||||
|
<a href="docs/themes.md">Themes</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">A lightweight, highly customizable dashboard that displays<br> your feeds in a beautiful, streamlined interface</p>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -256,8 +267,9 @@ Download and extract the executable from the [latest release](https://github.com
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
Glance can also be installed through the following 3rd party channels:
|
Glance can also be installed through the following 3rd party channels:
|
||||||
* [Proxmox VE Helper Script](https://community-scripts.github.io/ProxmoxVE/scripts?id=glance)
|
* [Proxmox VE Helper Script](https://community-scripts.org/scripts/glance?id=glance)
|
||||||
* [NixOS package](https://search.nixos.org/packages?channel=unstable&show=glance)
|
* [NixOS package](https://search.nixos.org/packages?channel=unstable&show=glance)
|
||||||
|
* [Hostinger](https://www.hostinger.com/vps/docker/glance)
|
||||||
* [Coolify.io](https://coolify.io/docs/services/glance/)
|
* [Coolify.io](https://coolify.io/docs/services/glance/)
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@@ -149,14 +149,14 @@ pages:
|
|||||||
columns:
|
columns:
|
||||||
- size: full
|
- size: full
|
||||||
widgets:
|
widgets:
|
||||||
$include: rss.yml
|
- $include: rss.yml
|
||||||
- name: News
|
- name: News
|
||||||
columns:
|
columns:
|
||||||
- size: full
|
- size: full
|
||||||
widgets:
|
widgets:
|
||||||
- type: group
|
- type: group
|
||||||
widgets:
|
widgets:
|
||||||
$include: rss.yml
|
- $include: rss.yml
|
||||||
- type: reddit
|
- type: reddit
|
||||||
subreddit: news
|
subreddit: news
|
||||||
```
|
```
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ pages:
|
|||||||
channels:
|
channels:
|
||||||
- theprimeagen
|
- theprimeagen
|
||||||
- j_blow
|
- j_blow
|
||||||
- piratesoftware
|
- giantwaffle
|
||||||
- cohhcarnage
|
- cohhcarnage
|
||||||
- christitustech
|
- christitustech
|
||||||
- EJ_SA
|
- EJ_SA
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 367 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
+80
-80
@@ -86,92 +86,92 @@ Pull requests with your page configurations are welcome!
|
|||||||
<summary>View config (requires Glance <code>v0.6.0</code> or higher)</summary>
|
<summary>View config (requires Glance <code>v0.6.0</code> or higher)</summary>
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Markets
|
- name: Markets
|
||||||
columns:
|
columns:
|
||||||
- size: small
|
- size: small
|
||||||
widgets:
|
widgets:
|
||||||
- type: markets
|
- type: markets
|
||||||
title: Indices
|
title: Indices
|
||||||
markets:
|
markets:
|
||||||
- symbol: SPY
|
- symbol: SPY
|
||||||
name: S&P 500
|
name: S&P 500
|
||||||
- symbol: DX-Y.NYB
|
- symbol: DX-Y.NYB
|
||||||
name: Dollar Index
|
name: Dollar Index
|
||||||
|
|
||||||
- type: markets
|
- type: markets
|
||||||
title: Crypto
|
title: Crypto
|
||||||
markets:
|
markets:
|
||||||
- symbol: BTC-USD
|
- symbol: BTC-USD
|
||||||
name: Bitcoin
|
name: Bitcoin
|
||||||
- symbol: ETH-USD
|
- symbol: ETH-USD
|
||||||
name: Ethereum
|
name: Ethereum
|
||||||
|
|
||||||
- type: markets
|
- type: markets
|
||||||
title: Stocks
|
title: Stocks
|
||||||
sort-by: absolute-change
|
sort-by: absolute-change
|
||||||
markets:
|
markets:
|
||||||
- symbol: NVDA
|
- symbol: NVDA
|
||||||
name: NVIDIA
|
name: NVIDIA
|
||||||
- symbol: AAPL
|
- symbol: AAPL
|
||||||
name: Apple
|
name: Apple
|
||||||
- symbol: MSFT
|
- symbol: MSFT
|
||||||
name: Microsoft
|
name: Microsoft
|
||||||
- symbol: GOOGL
|
- symbol: GOOGL
|
||||||
name: Google
|
name: Google
|
||||||
- symbol: AMD
|
- symbol: AMD
|
||||||
name: AMD
|
name: AMD
|
||||||
- symbol: RDDT
|
- symbol: RDDT
|
||||||
name: Reddit
|
name: Reddit
|
||||||
- symbol: AMZN
|
- symbol: AMZN
|
||||||
name: Amazon
|
name: Amazon
|
||||||
- symbol: TSLA
|
- symbol: TSLA
|
||||||
name: Tesla
|
name: Tesla
|
||||||
- symbol: INTC
|
- symbol: INTC
|
||||||
name: Intel
|
name: Intel
|
||||||
- symbol: META
|
- symbol: META
|
||||||
name: Meta
|
name: Meta
|
||||||
|
|
||||||
- size: full
|
- size: full
|
||||||
widgets:
|
widgets:
|
||||||
- type: rss
|
- type: rss
|
||||||
title: News
|
title: News
|
||||||
style: horizontal-cards
|
style: horizontal-cards
|
||||||
feeds:
|
feeds:
|
||||||
- url: https://feeds.bloomberg.com/markets/news.rss
|
- url: https://feeds.bloomberg.com/markets/news.rss
|
||||||
title: Bloomberg
|
title: Bloomberg
|
||||||
- url: https://moxie.foxbusiness.com/google-publisher/markets.xml
|
- url: https://moxie.foxbusiness.com/google-publisher/markets.xml
|
||||||
title: Fox Business
|
title: Fox Business
|
||||||
- url: https://moxie.foxbusiness.com/google-publisher/technology.xml
|
- url: https://moxie.foxbusiness.com/google-publisher/technology.xml
|
||||||
title: Fox Business
|
title: Fox Business
|
||||||
|
|
||||||
- type: group
|
- type: group
|
||||||
widgets:
|
widgets:
|
||||||
- type: reddit
|
- type: reddit
|
||||||
show-thumbnails: true
|
show-thumbnails: true
|
||||||
subreddit: technology
|
subreddit: technology
|
||||||
- type: reddit
|
- type: reddit
|
||||||
show-thumbnails: true
|
show-thumbnails: true
|
||||||
subreddit: wallstreetbets
|
subreddit: wallstreetbets
|
||||||
|
|
||||||
- type: videos
|
- type: videos
|
||||||
style: grid-cards
|
style: grid-cards
|
||||||
collapse-after-rows: 3
|
collapse-after-rows: 3
|
||||||
channels:
|
channels:
|
||||||
- UCvSXMi2LebwJEM1s4bz5IBA # New Money
|
- UCvSXMi2LebwJEM1s4bz5IBA # New Money
|
||||||
- UCV6KDgJskWaEckne5aPA0aQ # Graham Stephan
|
- UCV6KDgJskWaEckne5aPA0aQ # Graham Stephan
|
||||||
- UCAzhpt9DmG6PnHXjmJTvRGQ # Federal Reserve
|
- UCAzhpt9DmG6PnHXjmJTvRGQ # Federal Reserve
|
||||||
|
|
||||||
- size: small
|
- size: small
|
||||||
widgets:
|
widgets:
|
||||||
- type: rss
|
- type: rss
|
||||||
title: News
|
title: News
|
||||||
limit: 30
|
limit: 30
|
||||||
collapse-after: 13
|
collapse-after: 13
|
||||||
feeds:
|
feeds:
|
||||||
- url: https://www.ft.com/technology?format=rss
|
- url: https://www.ft.com/technology?format=rss
|
||||||
title: Financial Times
|
title: Financial Times
|
||||||
- url: https://feeds.a.dj.com/rss/RSSMarketsMain.xml
|
- url: https://feeds.a.dj.com/rss/RSSMarketsMain.xml
|
||||||
title: Wall Street Journal
|
title: Wall Street Journal
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,28 @@ theme:
|
|||||||
negative-color: 0 100 67
|
negative-color: 0 100 67
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Shades of Purple
|
||||||
|

|
||||||
|
```yaml
|
||||||
|
theme:
|
||||||
|
background-color: 243 33 25
|
||||||
|
contrast-multiplier: 1.2
|
||||||
|
primary-color: 50 100 49
|
||||||
|
positive-color: 98 82 71
|
||||||
|
negative-color: 12 77 52
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neon Pink
|
||||||
|

|
||||||
|
```yaml
|
||||||
|
theme:
|
||||||
|
background-color: 240 27 11
|
||||||
|
contrast-multiplier: 1.5
|
||||||
|
primary-color: 321 100 71
|
||||||
|
positive-color: 165 78 51
|
||||||
|
negative-color: 360 100 71
|
||||||
|
```
|
||||||
|
|
||||||
## Light
|
## Light
|
||||||
|
|
||||||
### Catppuccin Latte
|
### Catppuccin Latte
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
module github.com/glanceapp/glance
|
module github.com/glanceapp/glance
|
||||||
|
|
||||||
go 1.24.3
|
go 1.26.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/mmcdole/gofeed v1.3.0
|
github.com/mmcdole/gofeed v1.3.0
|
||||||
github.com/shirou/gopsutil/v4 v4.25.4
|
github.com/refraction-networking/utls v1.8.2
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.5
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
golang.org/x/crypto v0.38.0
|
golang.org/x/crypto v0.38.0
|
||||||
|
golang.org/x/net v0.40.0
|
||||||
golang.org/x/text v0.25.0
|
golang.org/x/text v0.25.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/ebitengine/purego v0.8.4 // indirect
|
github.com/ebitengine/purego v0.8.4 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||||
github.com/mmcdole/goxpp v1.1.1 // indirect
|
github.com/mmcdole/goxpp v1.1.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
@@ -28,6 +32,5 @@ require (
|
|||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
golang.org/x/net v0.40.0 // indirect
|
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||||
|
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -18,6 +20,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||||
|
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
|
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
|
||||||
@@ -33,8 +37,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
|
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package glance
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type SingleflightCall[T any] struct {
|
||||||
|
done chan struct{}
|
||||||
|
val T
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Singleflight[T any] struct {
|
||||||
|
fn func() (T, error)
|
||||||
|
mu sync.Mutex
|
||||||
|
current *SingleflightCall[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Singleflight[T]) Do() (T, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
if s.current != nil {
|
||||||
|
// Wait for the current call to finish and return its result
|
||||||
|
c := s.current
|
||||||
|
s.mu.Unlock()
|
||||||
|
<-c.done
|
||||||
|
return c.val, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate a new call
|
||||||
|
c := &SingleflightCall[T]{done: make(chan struct{})}
|
||||||
|
s.current = c
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
c.val, c.err = s.fn()
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.current = nil
|
||||||
|
s.mu.Unlock()
|
||||||
|
close(c.done)
|
||||||
|
return c.val, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSingleflight[T any](fn func() (T, error)) func() (T, error) {
|
||||||
|
sf := &Singleflight[T]{fn: fn}
|
||||||
|
return sf.Do
|
||||||
|
}
|
||||||
@@ -2,15 +2,22 @@ package glance
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
utls "github.com/refraction-networking/utls"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -158,7 +165,7 @@ func (widget *redditWidget) parseCustomCommentsURL(subreddit, postId, postPath s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (widget *redditWidget) fetchSubredditPosts() (forumPostList, error) {
|
func (widget *redditWidget) fetchSubredditPosts() (forumPostList, error) {
|
||||||
var client requestDoer = defaultHTTPClient
|
var client requestDoer = redditHTTPClient
|
||||||
var baseURL string
|
var baseURL string
|
||||||
var requestURL string
|
var requestURL string
|
||||||
var headers http.Header
|
var headers http.Header
|
||||||
@@ -212,6 +219,13 @@ func (widget *redditWidget) fetchSubredditPosts() (forumPostList, error) {
|
|||||||
}
|
}
|
||||||
request.Header = headers
|
request.Header = headers
|
||||||
|
|
||||||
|
loid, err := getRedditLoidCookie()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error fetching reddit loid cookie: %v\n", err)
|
||||||
|
return nil, errors.New("could not solve reddit challenge")
|
||||||
|
}
|
||||||
|
request.AddCookie(&http.Cookie{Name: "loid", Value: loid})
|
||||||
|
|
||||||
responseJson, err := decodeJsonFromRequest[subredditResponseJson](client, request)
|
responseJson, err := decodeJsonFromRequest[subredditResponseJson](client, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -308,3 +322,135 @@ func (widget *redditWidget) fetchNewAppAccessToken() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On Windows the default HTTP client works fine, but on Linux it seems to
|
||||||
|
// get detected and blocked by reddit (or cloudflare) presumably because of TLS fingerprinting,
|
||||||
|
// so we use uTLS to mimic a real browser's TLS fingerprint, which seems to work around the issue
|
||||||
|
var redditHTTPClient = &http.Client{Transport: &http2.Transport{
|
||||||
|
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpConn, err := (&net.Dialer{}).DialContext(ctx, network, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uconn := utls.UClient(tcpConn, &utls.Config{
|
||||||
|
ServerName: host,
|
||||||
|
}, utls.HelloFirefox_Auto)
|
||||||
|
|
||||||
|
if err := uconn.HandshakeContext(ctx); err != nil {
|
||||||
|
tcpConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uconn, nil
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
var (
|
||||||
|
redditChallengePattern = regexp.MustCompile(`await\(async \w+\s*=>\s*\w+\s*\+\s*\w+\)\("([^"]+)"\)`)
|
||||||
|
redditTokenPattern = regexp.MustCompile(`name="token"\s+value="([^"]+)"`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Allows all widget instances to share a single loid cookie, since we don't want to draw
|
||||||
|
// too much attention by making a lot of requests to the flow that allows us to obtain
|
||||||
|
// the cookie required to access the .json endpoints
|
||||||
|
var getRedditLoidCookie = func() func() (string, error) {
|
||||||
|
var lastUpdate time.Time
|
||||||
|
var cachedLoid string
|
||||||
|
|
||||||
|
return NewSingleflight(func() (string, error) {
|
||||||
|
// Caching for 6 hours is a bit arbitrary, presumably the cookie is valid for 24 hours,
|
||||||
|
// but we want to keep the cache time short in the event that a cookie becomes invalid
|
||||||
|
// for whatever reason, since we don't have a way to force refresh it
|
||||||
|
if time.Since(lastUpdate) < 6*time.Hour && cachedLoid != "" {
|
||||||
|
return cachedLoid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
loid, err := fetchRedditLoidCookie()
|
||||||
|
if err != nil {
|
||||||
|
if cachedLoid != "" {
|
||||||
|
fmt.Printf("Error fetching new reddit loid cookie, using cached value: %v\n", err)
|
||||||
|
return cachedLoid, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdate = time.Now()
|
||||||
|
cachedLoid = loid
|
||||||
|
return loid, nil
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
func fetchRedditLoidCookie() (string, error) {
|
||||||
|
request, err := http.NewRequest("GET", "https://www.reddit.com/", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
setBrowserUserAgentHeader(request)
|
||||||
|
|
||||||
|
response, err := redditHTTPClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("unexpected status code %d when requesting challenge page", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
challengeBody, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
challengeMatches := redditChallengePattern.FindSubmatch(challengeBody)
|
||||||
|
tokenMatches := redditTokenPattern.FindSubmatch(challengeBody)
|
||||||
|
|
||||||
|
if challengeMatches == nil {
|
||||||
|
return "", fmt.Errorf("no JS challenge found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenMatches == nil {
|
||||||
|
return "", fmt.Errorf("no token found in challenge page")
|
||||||
|
}
|
||||||
|
|
||||||
|
challengeStr := string(challengeMatches[1])
|
||||||
|
token := string(tokenMatches[1])
|
||||||
|
solution := challengeStr + challengeStr // the JS does: e + e
|
||||||
|
|
||||||
|
params := url.Values{
|
||||||
|
"solution": {solution},
|
||||||
|
"js_challenge": {"1"},
|
||||||
|
"token": {token},
|
||||||
|
}
|
||||||
|
request, err = http.NewRequest("GET", "https://www.reddit.com/?"+params.Encode(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
setBrowserUserAgentHeader(request)
|
||||||
|
|
||||||
|
response, err = redditHTTPClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("unexpected status code %d when submitting challenge solution", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cookie := range response.Cookies() {
|
||||||
|
if cookie.Name == "loid" {
|
||||||
|
return cookie.Value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no loid cookie found")
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func getBrowserUserAgentHeader() string {
|
|||||||
userAgentPersistentVersion.Store(rand.Int32N(5))
|
userAgentPersistentVersion.Store(rand.Int32N(5))
|
||||||
}
|
}
|
||||||
|
|
||||||
version := strconv.Itoa(130 + int(userAgentPersistentVersion.Load()))
|
version := strconv.Itoa(148 + int(userAgentPersistentVersion.Load()))
|
||||||
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + version + ".0) Gecko/20100101 Firefox/" + version + ".0"
|
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + version + ".0) Gecko/20100101 Firefox/" + version + ".0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user