Compare commits
127 Commits
031d5f545d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18ac13fd57 | ||
|
|
375fc1000a | ||
|
|
47c7aa74d3 | ||
|
|
a6aad2820d | ||
|
|
b26afcb325 | ||
|
|
613bfdf96e | ||
|
|
81507319dd | ||
|
|
50ad3bb3db | ||
|
|
07992b6636 | ||
|
|
9583234df5 | ||
|
|
2943362587 | ||
|
|
cbd4e32022 | ||
|
|
727b4fdea6 | ||
|
|
fd7dcafd4e | ||
|
|
4fbc96b96f | ||
|
|
0afb6535b6 | ||
|
|
10a40d4a54 | ||
|
|
4bdfbd1f06 | ||
|
|
44bcc0ba2e | ||
|
|
b17801060e | ||
|
|
99c1061a18 | ||
|
|
67840f6b28 | ||
|
|
cb44644475 | ||
|
|
6626077201 | ||
|
|
9c0129f968 | ||
|
|
8efffd0fe4 | ||
|
|
fe02d1624f | ||
|
|
75b7e21bec | ||
|
|
be148f5de5 | ||
|
|
a74f741298 | ||
|
|
fba3d635ea | ||
|
|
8f18cbae3a | ||
|
|
d73b63f43c | ||
|
|
f57560ebfc | ||
|
|
fc66d26c10 | ||
|
|
bd5ae006a1 | ||
|
|
11b215d026 | ||
|
|
d2f0466aae | ||
|
|
90887d3f11 | ||
|
|
582e35b237 | ||
|
|
8aeab5ada5 | ||
|
|
7dd5af0681 | ||
|
|
2897a55842 | ||
|
|
8b668af899 | ||
|
|
1c7dafc78b | ||
|
|
2e8c8d3d39 | ||
|
|
5e6fd81921 | ||
|
|
b9036ed950 | ||
|
|
bfbc2b6e7f | ||
|
|
6b51a0c0a3 | ||
|
|
137fdf07ed | ||
|
|
64c4d1908a | ||
|
|
aad5959350 | ||
|
|
f7f44d6615 | ||
|
|
14e80ac2c3 | ||
|
|
6259a4f179 | ||
|
|
3ac7ae63b6 | ||
|
|
786ea3ef8f | ||
|
|
c3bf31894c | ||
|
|
e8b6396760 | ||
|
|
5470576b10 | ||
|
|
8f0c62bd77 | ||
|
|
a0ddc7f25f | ||
|
|
e6f551913a | ||
|
|
c7375949c2 | ||
|
|
49064f1ea2 | ||
|
|
2f503497e9 | ||
|
|
5acbd00ee0 | ||
|
|
b48e596853 | ||
|
|
d7f098bea0 | ||
|
|
bd67eb0dfe | ||
|
|
7a94c74226 | ||
|
|
1eedf8fce8 | ||
|
|
bf23d6a9cf | ||
|
|
0271f84948 | ||
|
|
0e3e6c54de | ||
|
|
352eff2691 | ||
|
|
dbfd33f55e | ||
|
|
f2e828a9eb | ||
|
|
31f7facac7 | ||
|
|
0399fc9316 | ||
|
|
32a010e697 | ||
|
|
b200deb98f | ||
|
|
a4eedc7a86 | ||
|
|
dceebd5755 | ||
|
|
29cb008f38 | ||
|
|
e4e5529887 | ||
|
|
e2eb0afe06 | ||
|
|
754ceac95c | ||
|
|
4c9e6fbe35 | ||
|
|
ac642d0bdf | ||
|
|
a59857b549 | ||
|
|
efb75f74cb | ||
|
|
6f8e2e5c53 | ||
|
|
55c540e9c2 | ||
|
|
5ed296a3d2 | ||
|
|
83026a67d4 | ||
|
|
57e77e5d4e | ||
|
|
0628a678d8 | ||
|
|
49c44b9df8 | ||
|
|
f95563e849 | ||
|
|
ba429f6028 | ||
|
|
8b67437fd1 | ||
|
|
1fed2d648f | ||
|
|
a372df64a5 | ||
|
|
ab396d1833 | ||
|
|
a097814a62 | ||
|
|
18fd8dfac5 | ||
|
|
ce02422b1d | ||
|
|
ec1f0e007a | ||
|
|
19b6d180e7 | ||
|
|
e54c7a76f9 | ||
|
|
537eaf9801 | ||
|
|
baa97ab62d | ||
|
|
f57408d003 | ||
|
|
05587ac28e | ||
|
|
7f4f760407 | ||
|
|
7f97eecbca | ||
|
|
88ab880a8c | ||
|
|
e6d9e356ca | ||
|
|
b85df7bd31 | ||
|
|
a199e34730 | ||
|
|
61349beb85 | ||
|
|
a2d1d17e23 | ||
|
|
d77e35596e | ||
|
|
94f865174b | ||
|
|
0c5bb025bb |
19
go.mod
19
go.mod
@@ -2,16 +2,33 @@ module show-rss
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require modernc.org/sqlite v1.37.0
|
||||
require (
|
||||
golang.org/x/time v0.11.0
|
||||
modernc.org/sqlite v1.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/mmcdole/gofeed v1.3.0 // indirect
|
||||
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/net v0.4.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
modernc.org/libc v1.62.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.9.1 // indirect
|
||||
|
||||
43
go.sum
43
go.sum
@@ -1,24 +1,67 @@
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
|
||||
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
|
||||
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk=
|
||||
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
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=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
|
||||
|
||||
3
install_scratch.sh
Normal file
3
install_scratch.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
CGO_ENABLED=1 CC=x86_64-linux-musl-gcc go install -ldflags="-linkmode external -extldflags '-static'"
|
||||
7
main.go
7
main.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"show-rss/src/cmd"
|
||||
"syscall"
|
||||
@@ -17,5 +18,9 @@ func Main(ctx context.Context) error {
|
||||
ctx, can := signal.NotifyContext(ctx, syscall.SIGINT)
|
||||
defer can()
|
||||
|
||||
return cmd.Main(ctx)
|
||||
if err := cmd.Main(ctx, os.Args[1:]); err != nil && ctx.Err() == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package main_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
main "show-rss"
|
||||
"show-rss/src/db"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -11,7 +14,9 @@ func TestMain(t *testing.T) {
|
||||
ctx, can := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer can()
|
||||
|
||||
if err := main.Main(ctx); err != nil && ctx.Err() == nil {
|
||||
os.Args = []string{os.Args[0], "-db", path.Join(t.TempDir(), "db.db")}
|
||||
|
||||
if err := main.Main(db.Test(t, ctx)); err != nil && ctx.Err() == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
96
src/asses/db.go
Normal file
96
src/asses/db.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package asses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"show-rss/src/db"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func Next(ctx context.Context) (time.Time, error) {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
if deadline := Deadline(); time.Since(deadline) > time.Minute {
|
||||
return midnightLastNight().Add(24 * time.Hour), nil
|
||||
}
|
||||
|
||||
type Did struct {
|
||||
Did time.Time
|
||||
}
|
||||
result, err := db.QueryOne[Did](ctx, `
|
||||
SELECT executed_at AS "Did"
|
||||
FROM "asses.executions"
|
||||
ORDER BY executed_at DESC
|
||||
LIMIT 1
|
||||
`)
|
||||
return result.Did.Add(time.Hour), err
|
||||
}
|
||||
|
||||
func Record(ctx context.Context) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Exec(ctx, `INSERT INTO "asses.executions" (id, executed_at) VALUES ($1, $2)`, uuid.New().String(), time.Now())
|
||||
}
|
||||
|
||||
type last struct {
|
||||
T time.Time `json:"checked_at"`
|
||||
Cksum string `json:"cksum"`
|
||||
Modified time.Time `json:"modified"`
|
||||
}
|
||||
|
||||
func checkLast(ctx context.Context, p string) (last, error) {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return last{}, err
|
||||
}
|
||||
|
||||
return db.QueryOne[last](ctx, `
|
||||
SELECT checked_at, cksum, modified
|
||||
FROM "asses.checks"
|
||||
WHERE p=$1
|
||||
`, p)
|
||||
}
|
||||
|
||||
func checked(ctx context.Context, p, cksum string, modified time.Time) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cksum != "" {
|
||||
} else if err := db.Exec(ctx, `
|
||||
UPDATE "asses.checks"
|
||||
SET checked_at=$2, modified=$3
|
||||
WHERE p=$1
|
||||
`, p, time.Now(), modified); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return db.Exec(ctx, `
|
||||
INSERT INTO "asses.checks"
|
||||
(p, checked_at, cksum, modified)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
|
||||
ON CONFLICT DO UPDATE
|
||||
SET checked_at=$2, cksum=$3, modified=$4
|
||||
WHERE p=$1
|
||||
`, p, time.Now(), cksum, modified)
|
||||
}
|
||||
|
||||
func initDB(ctx context.Context) error {
|
||||
return db.InitializeSchema(ctx, "asses", []string{
|
||||
`CREATE TABLE "asses.executions" (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
executed_at TIMESTAMP NOT NULL
|
||||
)`,
|
||||
`CREATE TABLE "asses.checks" (
|
||||
p TEXT PRIMARY KEY NOT NULL,
|
||||
checked_at TIMESTAMP NOT NULL,
|
||||
cksum TEXT NOT NULL
|
||||
)`,
|
||||
`ALTER TABLE "asses.checks" ADD COLUMN "modified" TIMESTAMP`,
|
||||
})
|
||||
}
|
||||
29
src/asses/db_integration_test.go
Normal file
29
src/asses/db_integration_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package asses_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"show-rss/src/asses"
|
||||
"show-rss/src/db"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNextRecord(t *testing.T) {
|
||||
ctx := db.Test(t, context.Background())
|
||||
|
||||
if v, err := asses.Next(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if zero := v.IsZero(); !zero && time.Now().Hour() < 8 {
|
||||
t.Fatal(v)
|
||||
}
|
||||
|
||||
if err := asses.Record(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v, err := asses.Next(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if since := time.Since(v); since > time.Minute && time.Now().Hour() < 8 {
|
||||
t.Fatal(since)
|
||||
}
|
||||
}
|
||||
34
src/asses/db_test.go
Normal file
34
src/asses/db_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package asses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"show-rss/src/db"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLast(t *testing.T) {
|
||||
ctx := db.Test(t, context.Background())
|
||||
|
||||
if last, err := checkLast(ctx, "p"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !last.T.IsZero() || last.Cksum != "" {
|
||||
t.Fatal(last)
|
||||
}
|
||||
|
||||
modtime := time.Now().Add(-5 * time.Minute)
|
||||
if err := checked(ctx, "p", "cksum", modtime); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := checked(ctx, "p", "cksum", modtime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if last, err := checkLast(ctx, "p"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if last.T.IsZero() || last.Cksum != "cksum" {
|
||||
t.Fatal(last)
|
||||
} else if math.Abs(float64(last.Modified.Sub(modtime))) > float64(time.Second) {
|
||||
t.Fatalf("modified not uploaded: %v vs. %v (diff of %v)", last.Modified, modtime, last.Modified.Sub(modtime))
|
||||
}
|
||||
}
|
||||
15
src/asses/deadline.go
Normal file
15
src/asses/deadline.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package asses
|
||||
|
||||
import "time"
|
||||
|
||||
func Deadline() time.Time {
|
||||
return midnightLastNight().Add(8 * time.Hour) // midnight-8AM
|
||||
}
|
||||
|
||||
func midnightLastNight() time.Time {
|
||||
t, err := time.ParseInLocation("2006-01-02", time.Now().Format("2006-01-02"), time.Local)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
247
src/asses/deport.go
Normal file
247
src/asses/deport.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package asses
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Entrypoint(ctx context.Context, p string) error {
|
||||
return deport(ctx, p)
|
||||
}
|
||||
|
||||
func deport(ctx context.Context, p string) error {
|
||||
if os.Getenv("NO_DEPORT") != "" {
|
||||
log.Printf("would deport %s", p)
|
||||
return nil
|
||||
}
|
||||
|
||||
assStreams, err := assStreams(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assStreamIDs := make([]string, len(assStreams))
|
||||
for i, stream := range assStreams {
|
||||
assStreamIDs[i] = stream.id
|
||||
assF := path.Join(
|
||||
path.Dir(p),
|
||||
fmt.Sprintf(
|
||||
".%s.%s.%s.ass",
|
||||
path.Base(p),
|
||||
stream.id,
|
||||
stream.title,
|
||||
),
|
||||
)
|
||||
if err := ffmpeg(ctx, "-y", "-i", p, "-map", stream.id, assF); err != nil {
|
||||
return fmt.Errorf("failed to pull %s from %s: %w", stream.id, p, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := BestAssToSRT(ctx, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base := path.Base(p)
|
||||
withoutExt := strings.TrimSuffix(base, path.Ext(base))
|
||||
p2 := path.Join(path.Dir(p), fmt.Sprintf("%s.subless.mkv", withoutExt))
|
||||
args := []string{
|
||||
"-i", p,
|
||||
"-map", "0",
|
||||
}
|
||||
for _, assStream := range assStreams {
|
||||
args = append(args, "-map", "-"+assStream.id)
|
||||
}
|
||||
args = append(args,
|
||||
"-c", "copy",
|
||||
p2,
|
||||
)
|
||||
if err := ffmpeg(ctx, args...); err != nil {
|
||||
return err
|
||||
} else if err := os.Rename(p2, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
id string
|
||||
title string
|
||||
}
|
||||
|
||||
func assStreams(ctx context.Context, p string) ([]stream, error) {
|
||||
output, err := ffprobe(ctx, "-i", p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := []stream{}
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
} else if fields[0] != "Stream" {
|
||||
continue
|
||||
} else if !strings.Contains(fields[1], "(") {
|
||||
continue
|
||||
} else if fields[2] != "Subtitle:" {
|
||||
continue
|
||||
} else if fields[3] != "ass" {
|
||||
continue
|
||||
}
|
||||
field1 := fields[1]
|
||||
id := strings.Trim(strings.Split(field1, "(")[0], "#")
|
||||
title := strings.Trim(strings.Split(field1, "(")[1], "):")
|
||||
result = append(result, stream{
|
||||
id: id,
|
||||
title: title,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ffprobe(ctx context.Context, args ...string) (string, error) {
|
||||
return execc(ctx, "ffprobe", args...)
|
||||
}
|
||||
|
||||
func ffmpeg(ctx context.Context, args ...string) error {
|
||||
std, err := execc(ctx, "ffmpeg", args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("(%w) %s", err, std)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func execc(ctx context.Context, bin string, args ...string) (string, error) {
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
cmd := exec.CommandContext(ctx, bin, args...)
|
||||
cmd.Stdin = nil
|
||||
cmd.Stderr = stdout
|
||||
cmd.Stdout = stdout
|
||||
|
||||
err := cmd.Run()
|
||||
return string(stdout.Bytes()), err
|
||||
}
|
||||
|
||||
func BestAssToSRT(ctx context.Context, p string) error {
|
||||
asses, err := filepath.Glob(path.Join(
|
||||
path.Dir(p),
|
||||
fmt.Sprintf(".%s.*.ass", path.Base(p)),
|
||||
))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srts := []string{}
|
||||
for _, ass := range asses {
|
||||
srt, err := assToSRT(ctx, ass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srts = append(srts, srt)
|
||||
}
|
||||
|
||||
srts = SRTsByGoodness(srts)
|
||||
|
||||
for i := range srts {
|
||||
if i == 0 {
|
||||
base := path.Base(p)
|
||||
withoutExt := strings.TrimSuffix(base, path.Ext(base))
|
||||
|
||||
srt := path.Join(path.Dir(p), fmt.Sprintf("%s.srt", withoutExt))
|
||||
if err := os.Rename(srts[i], srt); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
os.Remove(srts[i])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func assToSRT(ctx context.Context, ass string) (string, error) {
|
||||
ctx, can := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer can()
|
||||
|
||||
srt := fmt.Sprintf("%s.srt", strings.TrimSuffix(ass, ".ass"))
|
||||
if _, err := os.Stat(srt); err == nil {
|
||||
return srt, nil
|
||||
}
|
||||
|
||||
if err := ffmpeg(ctx, "-y", "-i", ass, srt); err != nil {
|
||||
if ctx.Err() == nil {
|
||||
log.Printf("ffmpeg failed to process %s; removing", ass)
|
||||
os.Remove(ass)
|
||||
}
|
||||
return srt, err
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(srt)
|
||||
if err != nil {
|
||||
return srt, err
|
||||
}
|
||||
before := len(b)
|
||||
b = regexp.MustCompile(`size="[^"]*"`).ReplaceAll(b, []byte{})
|
||||
if after := len(b); before == after {
|
||||
} else if err := os.WriteFile(srt, b, os.ModePerm); err != nil {
|
||||
return srt, err
|
||||
}
|
||||
return srt, nil
|
||||
}
|
||||
|
||||
func SRTsByGoodness(srts []string) []string {
|
||||
skippers := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)lat.*amer`),
|
||||
regexp.MustCompile(`(?i)signs`),
|
||||
regexp.MustCompile(`(?i)rus`),
|
||||
regexp.MustCompile(`(?i)por`),
|
||||
regexp.MustCompile(`(?i)ita`),
|
||||
regexp.MustCompile(`(?i)fre`),
|
||||
regexp.MustCompile(`(?i)spa`),
|
||||
regexp.MustCompile(`(?i)ger`),
|
||||
regexp.MustCompile(`(?i)ara`),
|
||||
regexp.MustCompile(`(?i)jpn`),
|
||||
regexp.MustCompile(`(?i)urop`),
|
||||
regexp.MustCompile(`(?i)razil`),
|
||||
regexp.MustCompile(`(?i)Deu`),
|
||||
regexp.MustCompile(`(?i)ara`),
|
||||
}
|
||||
|
||||
keepers := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)^eng$`),
|
||||
}
|
||||
|
||||
srts = slices.Clone(srts)
|
||||
slices.SortFunc(srts, func(a, b string) int {
|
||||
a = strings.ToLower(a)
|
||||
b = strings.ToLower(b)
|
||||
for _, skipper := range skippers {
|
||||
if skipper.MatchString(b) {
|
||||
return -1
|
||||
} else if skipper.MatchString(a) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
for _, keeper := range keepers {
|
||||
if keeper.MatchString(a) {
|
||||
return -1
|
||||
} else if keeper.MatchString(b) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
})
|
||||
return srts
|
||||
}
|
||||
47
src/asses/deport_test.go
Normal file
47
src/asses/deport_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package asses_test
|
||||
|
||||
import (
|
||||
"show-rss/src/asses"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSRTsByGoodness(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
given []string
|
||||
want string
|
||||
}{
|
||||
"eng": {
|
||||
given: []string{"a", "eng"},
|
||||
want: "eng",
|
||||
},
|
||||
"eng nocap": {
|
||||
given: []string{"A", "eng"},
|
||||
want: "eng",
|
||||
},
|
||||
".Apothecary_Diaries_S02E19.mkv.0:9.ita.ass": {
|
||||
given: []string{
|
||||
".Apothecary_Diaries_S02E19.mkv.0:10.rus.srt",
|
||||
".Apothecary_Diaries_S02E19.mkv.0:2.eng.srt",
|
||||
".Apothecary_Diaries_S02E19.mkv.0:3.por.srt",
|
||||
".Apothecary_Diaries_S02E19.mkv.0:4.spa.srt",
|
||||
".Apothecary_Diaries_S02E19.mkv.0:5.spa.srt",
|
||||
".Apothecary_Diaries_S02E19.mkv.0:6.ara.srt",
|
||||
".Apothecary_Diaries_S02E19.mkv.0:7.fre.srt",
|
||||
".Apothecary_Diaries_S02E19.mkv.0:8.ger.srt",
|
||||
".Apothecary_Diaries_S02E19.mkv.0:9.ita.srt",
|
||||
},
|
||||
want: ".Apothecary_Diaries_S02E19.mkv.0:2.eng.srt",
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
name := name
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := asses.SRTsByGoodness(c.given)
|
||||
if got[0] != c.want {
|
||||
t.Errorf("expected %s but got %s (%+v)", c.want, got[0], got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
125
src/asses/one.go
Normal file
125
src/asses/one.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package asses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"show-rss/src/slow"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
var EnvCksumBPS = func() int {
|
||||
s := os.Getenv("CKSUM_BPS")
|
||||
if s == "" {
|
||||
return 50_000_000
|
||||
}
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil || n < 1 {
|
||||
panic(err)
|
||||
}
|
||||
return n
|
||||
}()
|
||||
|
||||
func One(ctx context.Context, p string) error {
|
||||
shortp := path.Join("...", path.Base(path.Dir(p)), path.Base(p))
|
||||
|
||||
last, err := checkLast(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
threshold := 20 + rand.New(rand.NewSource(func() int64 {
|
||||
b := md5.New().Sum([]byte(p))
|
||||
var sum int64
|
||||
for _, c := range b {
|
||||
sum += int64(c)
|
||||
sum *= int64(c)
|
||||
}
|
||||
return sum
|
||||
}())).Int()%10
|
||||
if daysSince := int(time.Since(last.T).Hours() / 24); daysSince > threshold {
|
||||
log.Printf("asses.One(%s) // no modified check as %vd since last check", shortp, daysSince)
|
||||
} else if stat, err := os.Stat(p); err != nil {
|
||||
return fmt.Errorf("cannot stat %s: %w", p, err)
|
||||
} else if stat.ModTime() == last.Modified {
|
||||
//log.Printf("asses.One(%s) // unmodified since %v", shortp, last.T)
|
||||
return nil
|
||||
} else {
|
||||
log.Printf("asses.One(%s) // modified (%v) is now %v", shortp, last.Modified, stat.ModTime())
|
||||
}
|
||||
|
||||
doCksum := true
|
||||
if err := func() error {
|
||||
if len(last.Cksum) > 0 {
|
||||
if last.Modified.IsZero() {
|
||||
doCksum = false
|
||||
log.Printf("asses.One(%s) // assume cksum unchanged given null modified ", shortp)
|
||||
return nil
|
||||
}
|
||||
|
||||
cksum, err := Cksum(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cksum == last.Cksum {
|
||||
log.Printf("asses.One(%s) // cksum unchanged since %v", shortp, last.T)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("asses.deport(%s)...", shortp)
|
||||
if err := deport(ctx, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("asses.transcode(%s)...", shortp)
|
||||
if err := transcode(ctx, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cksum string
|
||||
if doCksum {
|
||||
var err error
|
||||
cksum, err = Cksum(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stat, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checked(ctx, p, cksum, stat.ModTime()); err != nil {
|
||||
log.Printf("failed to mark %s checked: %v", shortp, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Cksum(ctx context.Context, p string) (string, error) {
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
hasher := md5.New()
|
||||
_, err = io.Copy(hasher, slow.NewReader(ctx, rate.Limit(EnvCksumBPS), f))
|
||||
return base64.StdEncoding.EncodeToString(hasher.Sum(nil)), err
|
||||
}
|
||||
44
src/asses/one_test.go
Normal file
44
src/asses/one_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package asses_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"show-rss/src/asses"
|
||||
"show-rss/src/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOne(t *testing.T) {
|
||||
ctx := db.Test(t, context.Background())
|
||||
|
||||
os.Setenv("DO_TRANSCODE", "true")
|
||||
|
||||
d := t.TempDir()
|
||||
b, _ := os.ReadFile(path.Join("testdata", "survivor_au_S11E12.smoller.mkv"))
|
||||
p := path.Join(d, "f.mkv")
|
||||
os.WriteFile(p, b, os.ModePerm)
|
||||
|
||||
t.Logf("initial cksum...")
|
||||
cksum, _ := asses.Cksum(context.Background(), p)
|
||||
|
||||
t.Logf("one && one...")
|
||||
if err := asses.One(ctx, p); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := asses.One(ctx, p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("test -f...")
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
t.Fatalf("lost original mkv: %v", err)
|
||||
} else if _, err := os.Stat(path.Join(d, "f.srt")); err != nil {
|
||||
t.Fatalf("no new srt: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("final cksum...")
|
||||
newCksum, _ := asses.Cksum(context.Background(), p)
|
||||
if cksum == newCksum {
|
||||
t.Fatalf("cksum unchanged")
|
||||
}
|
||||
}
|
||||
BIN
src/asses/testdata/survivor_au_S11E12.smoller.mkv
vendored
Normal file
BIN
src/asses/testdata/survivor_au_S11E12.smoller.mkv
vendored
Normal file
Binary file not shown.
64
src/asses/transcode.go
Normal file
64
src/asses/transcode.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package asses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func EntrypointTranscode(ctx context.Context, p string) error {
|
||||
return transcode(ctx, p)
|
||||
}
|
||||
|
||||
func transcode(ctx context.Context, p string) error {
|
||||
if os.Getenv("NO_TRANSCODE") != "" || os.Getenv("DO_TRANSCODE") == "" {
|
||||
log.Printf("would transcode %s but $NO_TRANSCODE=x or $DO_TRANSCODE=", p)
|
||||
return nil
|
||||
}
|
||||
|
||||
output, err := ffprobe(ctx, "-i", p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h264 := slices.ContainsFunc(strings.Split(output, "\n"), func(line string) bool {
|
||||
return strings.Contains(line, "tream #") && strings.Contains(line, "Video: ") && strings.Contains(line, "h264")
|
||||
})
|
||||
aac := slices.ContainsFunc(strings.Split(output, "\n"), func(line string) bool {
|
||||
return strings.Contains(line, "tream #") && strings.Contains(line, "Audio: ") && strings.Contains(line, "aac")
|
||||
})
|
||||
|
||||
if h264 && aac {
|
||||
return nil
|
||||
}
|
||||
|
||||
p2 := p + ".en" + path.Ext(p)
|
||||
if err := ffmpeg(ctx, "-y",
|
||||
"-i", p,
|
||||
"-vcodec", "libx264",
|
||||
"-acodec", "aac",
|
||||
p2,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output2, err := ffprobe(ctx, "-i", p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
df := func(line string) bool {
|
||||
return !strings.Contains(line, "tream #")
|
||||
}
|
||||
originalStreams := slices.DeleteFunc(strings.Split(output, "\n"), df)
|
||||
newStreams := slices.DeleteFunc(strings.Split(output2, "\n"), df)
|
||||
if len(originalStreams) != len(newStreams) {
|
||||
return fmt.Errorf("stream count changed from transcode")
|
||||
}
|
||||
|
||||
return os.Rename(p2, p)
|
||||
}
|
||||
70
src/cmd/asses/main.go
Normal file
70
src/cmd/asses/main.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package asses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"log"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"show-rss/src/asses"
|
||||
"show-rss/src/cron"
|
||||
)
|
||||
|
||||
var rootDs = []string{
|
||||
"/volume1/video/Bel/Anime",
|
||||
"/volume1/video/QT/TV",
|
||||
}
|
||||
|
||||
type CB func(context.Context, string) error
|
||||
|
||||
func Main(ctx context.Context) error {
|
||||
return cron.Cron(ctx, asses.Next, One)
|
||||
}
|
||||
|
||||
func One(ctx context.Context) error {
|
||||
ctx, can := context.WithDeadline(ctx, asses.Deadline())
|
||||
defer can()
|
||||
|
||||
lastD := ""
|
||||
if err := OneWith(ctx, rootDs, func(ctx context.Context, p string) error {
|
||||
if d := path.Dir(p); d != lastD {
|
||||
log.Printf("asses.One(%s/...)...", d)
|
||||
lastD = d
|
||||
}
|
||||
if err := asses.One(ctx, p); err != nil {
|
||||
log.Printf("asses.One(.../%s/%s)...: err: %v", path.Base(path.Dir(p)), path.Base(p), err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return asses.Record(ctx)
|
||||
}
|
||||
|
||||
func OneWith(ctx context.Context, rootds []string, cb CB) error {
|
||||
for _, rootd := range rootds {
|
||||
if err := one(ctx, rootd, cb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func one(ctx context.Context, rootd string, cb CB) error {
|
||||
return filepath.WalkDir(rootd, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if path.Ext(p) != ".mkv" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return cb(ctx, p)
|
||||
})
|
||||
}
|
||||
28
src/cmd/asses/main_test.go
Normal file
28
src/cmd/asses/main_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package asses_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"show-rss/src/cmd/asses"
|
||||
"show-rss/src/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOneWith(t *testing.T) {
|
||||
ctx := db.Test(t, context.Background())
|
||||
|
||||
d := t.TempDir()
|
||||
os.MkdirAll(path.Join(d, "a", "b", "c"), os.ModePerm)
|
||||
os.WriteFile(path.Join(d, "a", "f.mkv"), []byte{}, os.ModePerm)
|
||||
|
||||
if err := asses.OneWith(ctx, []string{d}, func(_ context.Context, p string) error {
|
||||
t.Logf("%q", p)
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,47 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"os"
|
||||
"show-rss/src/cleanup"
|
||||
"show-rss/src/db"
|
||||
"show-rss/src/server"
|
||||
)
|
||||
|
||||
func Config(ctx context.Context) (context.Context, error) {
|
||||
ctx, err := db.Inject(ctx, "/tmp/f.db")
|
||||
type Flags struct {
|
||||
DB string
|
||||
Port int
|
||||
Entrypoint Entrypoint
|
||||
Pos []string
|
||||
}
|
||||
|
||||
func NewFlags(args []string) (Flags, error) {
|
||||
var result Flags
|
||||
|
||||
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
fs.StringVar(&result.DB, "db", "/tmp/f.db", "path to sqlite.db")
|
||||
fs.IntVar(&result.Port, "p", 10000, "port for http")
|
||||
fs.Var(&result.Entrypoint, "e", "entrypoint")
|
||||
err := fs.Parse(args)
|
||||
result.Pos = fs.Args()
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func Config(ctx context.Context, args []string) (context.Context, func(), Flags, error) {
|
||||
flags, err := NewFlags(args)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
return ctx, nil, flags, err
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
ctx, err = db.Inject(ctx, flags.DB)
|
||||
if err != nil {
|
||||
return ctx, nil, flags, err
|
||||
}
|
||||
|
||||
ctx = server.Inject(ctx, flags.Port)
|
||||
|
||||
return ctx, func() {
|
||||
cleanup.Extract(ctx)()
|
||||
}, flags, nil
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"show-rss/src/db"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Main(ctx context.Context) error {
|
||||
c := time.NewTicker(time.Minute)
|
||||
defer c.Stop()
|
||||
for {
|
||||
if err := one(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-c.C:
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func one(ctx context.Context) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
func initDB(ctx context.Context) error {
|
||||
return db.Exec(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS feeds (
|
||||
id SERIAL
|
||||
);
|
||||
ALTER TABLE feeds ADD COLUMN IF NOT EXISTS b TEXT;
|
||||
ALTER TABLE feeds ADD COLUMN IF NOT EXISTS b TEXT;
|
||||
`)
|
||||
}
|
||||
133
src/cmd/fetch/main.go
Normal file
133
src/cmd/fetch/main.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"show-rss/src/cron"
|
||||
"show-rss/src/feeds"
|
||||
"show-rss/src/webhooks"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Main(ctx context.Context) error {
|
||||
return cron.Cron(ctx, feeds.Next, One)
|
||||
}
|
||||
|
||||
func One(ctx context.Context) error {
|
||||
return feeds.ForEach(ctx, func(feed feeds.Feed) error {
|
||||
if err := one(ctx, feed); err != nil {
|
||||
return fmt.Errorf("failed to cron %s (%+v): %w", feed.Entry.ID, feed.Version, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func one(ctx context.Context, feed feeds.Feed) error {
|
||||
if should, err := feed.ShouldExecute(); err != nil {
|
||||
return err
|
||||
} else if !should {
|
||||
return nil
|
||||
}
|
||||
log.Printf("fetching %s", feed.Version.URL)
|
||||
|
||||
items, err := feed.Fetch(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("fetched feed %s: %d items", feed.Version.URL, len(items))
|
||||
|
||||
for _, item := range items {
|
||||
if err := oneItem(ctx, feed, item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return feed.Executed(ctx)
|
||||
}
|
||||
|
||||
func oneItem(ctx context.Context, feed feeds.Feed, item feeds.Item) error {
|
||||
type Arg struct {
|
||||
Feed feeds.Feed
|
||||
Item feeds.Item
|
||||
}
|
||||
arg := Arg{
|
||||
Feed: feed,
|
||||
Item: item,
|
||||
}
|
||||
|
||||
wmethod, wurl, wbody := feed.Webhook(ctx)
|
||||
|
||||
method, err := render(wmethod, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wurl, err = render(wurl, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := render(wbody, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user *url.Userinfo
|
||||
if u, _ := url.Parse(wurl); u.User != nil {
|
||||
user = u.User
|
||||
u.User = &url.Userinfo{}
|
||||
wurl = u.String()
|
||||
}
|
||||
|
||||
if did, err := webhooks.Did(ctx, method, wurl, body); err != nil || did {
|
||||
return err
|
||||
}
|
||||
log.Printf("webhooking %s %s: %s", method, wurl, body)
|
||||
|
||||
req, err := http.NewRequest(method, wurl, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if user != nil {
|
||||
u := user.Username()
|
||||
p, _ := user.Password()
|
||||
req.SetBasicAuth(u, p)
|
||||
}
|
||||
|
||||
c := http.Client{
|
||||
Timeout: time.Minute,
|
||||
Transport: &http.Transport{DisableKeepAlives: true},
|
||||
}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer io.Copy(io.Discard, resp.Body)
|
||||
|
||||
if resp.StatusCode > 204 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed %s %s w/ %s: (%d) %s", method, wurl, body, resp.StatusCode, b)
|
||||
}
|
||||
|
||||
return webhooks.Record(ctx, method, wurl, body)
|
||||
}
|
||||
|
||||
func render(text string, arg any) (string, error) {
|
||||
tmpl, err := template.New("render").Parse(text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer(nil)
|
||||
err = tmpl.Execute(b, arg)
|
||||
return string(b.Bytes()), err
|
||||
}
|
||||
107
src/cmd/fetch/main_test.go
Normal file
107
src/cmd/fetch/main_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package fetch_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"show-rss/src/cmd/fetch"
|
||||
"show-rss/src/db"
|
||||
"show-rss/src/feeds"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestOne(t *testing.T) {
|
||||
was := feeds.ProxyU
|
||||
feeds.ProxyU = nil
|
||||
t.Cleanup(func() { feeds.ProxyU = was })
|
||||
|
||||
ctx, can := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer can()
|
||||
|
||||
for name, aCtx := range map[string]func(t *testing.T) context.Context{
|
||||
"empty": func(t *testing.T) context.Context {
|
||||
return db.Test(t, ctx)
|
||||
},
|
||||
"feeds": func(t *testing.T) context.Context {
|
||||
gets := []string{}
|
||||
sURL := "http://localhost:10001/"
|
||||
s := &http.Server{
|
||||
Addr: ":10001",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gets = append(gets, r.URL.String())
|
||||
rb, _ := io.ReadAll(r.Body)
|
||||
t.Logf("serving fetch %s (%s)", gets[len(gets)-1], rb)
|
||||
|
||||
switch r.URL.Query().Get("idx") {
|
||||
case "0":
|
||||
case "1":
|
||||
case "10":
|
||||
case "11":
|
||||
default:
|
||||
t.Logf("%s => 404", r.URL)
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
b, _ := os.ReadFile(fmt.Sprintf("testdata/%s.rss", r.URL.Query().Get("idx")))
|
||||
io.Copy(w, bytes.NewReader(b))
|
||||
}),
|
||||
}
|
||||
go s.ListenAndServe()
|
||||
t.Cleanup(func() { s.Close() })
|
||||
t.Cleanup(func() {
|
||||
if len(gets) != 1+2+2*2 { // healthcheck, (id=0,1) for feeds, 3 matching webhooks deduped down to 2
|
||||
t.Errorf("didn't call urls: %+v", gets)
|
||||
}
|
||||
})
|
||||
|
||||
for {
|
||||
resp, err := http.Get(sURL)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
ctx := db.Test(t, ctx)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
if _, err := feeds.Insert(ctx, fmt.Sprintf("%s?idx=%d", sURL, i), "* * * * *", "matches", http.MethodHead, fmt.Sprintf("%s?idx=1%d", sURL, i), "{{.Item.Title}}"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
},
|
||||
} {
|
||||
name := name
|
||||
aCtx := aCtx
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("same ctx", func(t *testing.T) {
|
||||
ctx := aCtx(t)
|
||||
for i := 0; i < 2; i++ {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
if err := fetch.One(ctx); err != nil && ctx.Err() == nil {
|
||||
t.Fatalf("failed %d: %v", i, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new ctx", func(t *testing.T) {
|
||||
for i := 0; i < 2; i++ {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
ctx := aCtx(t)
|
||||
if err := fetch.One(ctx); err != nil && ctx.Err() == nil {
|
||||
t.Fatalf("failed %d: %v", i, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
29
src/cmd/fetch/testdata/0.rss
vendored
Normal file
29
src/cmd/fetch/testdata/0.rss
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<rdf:RDF>
|
||||
<item>
|
||||
<title>title matches</title>
|
||||
<link>link</link>
|
||||
<description>description</description>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>title</title>
|
||||
<link>link matches</link>
|
||||
<description>description</description>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>title</title>
|
||||
<link>link</link>
|
||||
<description>description matches</description>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>title</title>
|
||||
<link>link</link>
|
||||
<description>description</description>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
</item>
|
||||
</rdf:RDF>
|
||||
30
src/cmd/fetch/testdata/1.rss
vendored
Normal file
30
src/cmd/fetch/testdata/1.rss
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<rdf:RDF>
|
||||
<item>
|
||||
<title>1 title matches</title>
|
||||
<link>1 link</link>
|
||||
<description>1 description</description>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>1 title</title>
|
||||
<link>1 link matches</link>
|
||||
<description>1 description</description>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>1 title</title>
|
||||
<link>1 link</link>
|
||||
<description>1 description matches</description>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>1 title</title>
|
||||
<link>1 link</link>
|
||||
<description>1 description</description>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
</item>
|
||||
</rdf:RDF>
|
||||
|
||||
114
src/cmd/main.go
114
src/cmd/main.go
@@ -4,35 +4,118 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"show-rss/src/cmd/cron"
|
||||
inass "show-rss/src/asses"
|
||||
"show-rss/src/cmd/asses"
|
||||
"show-rss/src/cmd/fetch"
|
||||
"show-rss/src/cmd/server"
|
||||
"show-rss/src/pool"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Main(ctx context.Context) error {
|
||||
func Main(ctx context.Context, args []string) error {
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
defer can()
|
||||
|
||||
ctx, err := Config(ctx)
|
||||
ctx, can, flags, err := Config(ctx, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inject: %w", err)
|
||||
}
|
||||
defer can()
|
||||
|
||||
foos := map[string]func(context.Context) error{
|
||||
"server": server.Main,
|
||||
"cron": cron.Main,
|
||||
}
|
||||
p := pool.New(len(foos))
|
||||
defer p.Wait(ctx)
|
||||
|
||||
for k, foo := range foos {
|
||||
if err := p.Go(ctx, k, runner(ctx, k, foo)); err != nil {
|
||||
return fmt.Errorf("failed to go %s: %v", k, err)
|
||||
switch flags.Entrypoint {
|
||||
case Defacto:
|
||||
foos := map[string]func(context.Context) error{
|
||||
"server": server.Main,
|
||||
"fetch": fetch.Main,
|
||||
"asses": asses.Main,
|
||||
}
|
||||
}
|
||||
p := pool.New(len(foos))
|
||||
defer p.Wait(ctx)
|
||||
|
||||
return p.Wait(ctx)
|
||||
for k, foo := range foos {
|
||||
if err := p.Go(ctx, k, runner(ctx, k, foo)); err != nil {
|
||||
return fmt.Errorf("failed to go %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
return p.Wait(ctx)
|
||||
case DeportAss:
|
||||
for _, pos := range flags.Pos {
|
||||
if err := inass.Entrypoint(ctx, pos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case BestAssToSRT:
|
||||
errs := []string{}
|
||||
for _, pos := range flags.Pos {
|
||||
if err := inass.BestAssToSRT(ctx, pos); err != nil {
|
||||
err = fmt.Errorf("[%s] %w", pos, err)
|
||||
log.Println(err)
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("errors: %+v", errs)
|
||||
}
|
||||
return nil
|
||||
case Transcode:
|
||||
for _, pos := range flags.Pos {
|
||||
log.Printf("transcoding %q...", pos)
|
||||
if err := inass.EntrypointTranscode(ctx, pos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
panic(flags.Entrypoint.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Entrypoint int
|
||||
|
||||
const (
|
||||
Defacto Entrypoint = iota
|
||||
DeportAss
|
||||
BestAssToSRT
|
||||
Transcode
|
||||
)
|
||||
|
||||
func (e *Entrypoint) Set(s string) error {
|
||||
switch s {
|
||||
case Defacto.String():
|
||||
*e = Defacto
|
||||
case DeportAss.String():
|
||||
*e = DeportAss
|
||||
case BestAssToSRT.String():
|
||||
*e = BestAssToSRT
|
||||
case Transcode.String():
|
||||
*e = Transcode
|
||||
default:
|
||||
return fmt.Errorf("%s nin (%s)", s, strings.Join([]string{
|
||||
Defacto.String(),
|
||||
DeportAss.String(),
|
||||
BestAssToSRT.String(),
|
||||
Transcode.String(),
|
||||
}, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Entrypoint) String() string {
|
||||
switch e {
|
||||
case Defacto:
|
||||
return ""
|
||||
case DeportAss:
|
||||
return "deport-ass"
|
||||
case BestAssToSRT:
|
||||
return "best-ass-to-srt"
|
||||
case Transcode:
|
||||
return "transcode"
|
||||
}
|
||||
panic("cannot serialize entrypoint")
|
||||
}
|
||||
|
||||
func runner(ctx context.Context, k string, foo func(context.Context) error) func() error {
|
||||
@@ -49,6 +132,5 @@ func runner(ctx context.Context, k string, foo func(context.Context) error) func
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
69
src/cmd/server/handler/feeds.go
Normal file
69
src/cmd/server/handler/feeds.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"show-rss/src/feeds"
|
||||
)
|
||||
|
||||
func (h Handler) feeds(w http.ResponseWriter, r *http.Request) error {
|
||||
switch r.Method {
|
||||
case http.MethodDelete:
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.feedsDelete(r.Context(), r.URL.Query().Get("id")); err != nil {
|
||||
return err
|
||||
}
|
||||
case http.MethodPost, http.MethodPut:
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.URL.Query().Has("delete") {
|
||||
if err := h.feedsDelete(r.Context(), r.URL.Query().Get("id")); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := h.feedsUpsert(r.Context(), r.URL.Query().Get("id"), r.Form); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
u2 := *r.URL
|
||||
u2.RawQuery = ""
|
||||
u2.Path = "/"
|
||||
http.Redirect(w, r, u2.String(), http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h Handler) feedsDelete(ctx context.Context, id string) error {
|
||||
return feeds.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (h Handler) feedsUpsert(ctx context.Context, id string, form url.Values) error {
|
||||
var req feeds.Version
|
||||
for k, v := range map[string]*string{
|
||||
"Cron": &req.Cron,
|
||||
"Pattern": &req.Pattern,
|
||||
"URL": &req.URL,
|
||||
"WebhookBody": &req.WebhookBody,
|
||||
"WebhookMethod": &req.WebhookMethod,
|
||||
"WebhookURL": &req.WebhookURL,
|
||||
} {
|
||||
if *v = form.Get(k); *v == "" {
|
||||
return fmt.Errorf("no ?%s in %s", k, form.Encode())
|
||||
}
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
_, err := feeds.Insert(ctx, req.URL, req.Cron, req.Pattern, req.WebhookMethod, req.WebhookURL, req.WebhookBody)
|
||||
return err
|
||||
}
|
||||
return feeds.Update(ctx, id, req.URL, req.Cron, req.Pattern, req.WebhookMethod, req.WebhookURL, req.WebhookBody)
|
||||
}
|
||||
64
src/cmd/server/handler/feeds_test.go
Normal file
64
src/cmd/server/handler/feeds_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"show-rss/src/cmd/server/handler"
|
||||
"show-rss/src/db"
|
||||
"show-rss/src/feeds"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFeeds(t *testing.T) {
|
||||
ctx := db.Test(t, context.Background())
|
||||
h := handler.New(ctx)
|
||||
|
||||
t.Run("happy", func(t *testing.T) {
|
||||
body := make(url.Values)
|
||||
body.Set("URL", "url")
|
||||
body.Set("Cron", "cron")
|
||||
body.Set("Pattern", "pattern")
|
||||
body.Set("WebhookMethod", "wmethod")
|
||||
body.Set("WebhookURL", "wurl")
|
||||
body.Set("WebhookBody", "wbody")
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodPost, "/v1/feeds", strings.NewReader(body.Encode()))
|
||||
r = r.WithContext(ctx)
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Errorf("(%d) %s", w.Code, w.Body.Bytes())
|
||||
}
|
||||
found := false
|
||||
if err := feeds.ForEach(ctx, func(f feeds.Feed) error {
|
||||
t.Logf("%+v", f)
|
||||
if f.Version.URL != "url" {
|
||||
t.Errorf("bad url")
|
||||
}
|
||||
if f.Version.Cron != "cron" {
|
||||
t.Errorf("bad cron")
|
||||
}
|
||||
if f.Version.Pattern != "pattern" {
|
||||
t.Errorf("bad pattern")
|
||||
}
|
||||
if f.Version.WebhookMethod != "wmethod" {
|
||||
t.Errorf("bad wmethod")
|
||||
}
|
||||
if f.Version.WebhookURL != "wurl" {
|
||||
t.Errorf("bad wurl")
|
||||
}
|
||||
if f.Version.WebhookBody != "wbody" {
|
||||
t.Errorf("bad wbody")
|
||||
}
|
||||
found = true
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
} else if !found {
|
||||
t.Error(found)
|
||||
}
|
||||
})
|
||||
}
|
||||
43
src/cmd/server/handler/handler.go
Normal file
43
src/cmd/server/handler/handler.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func New(ctx context.Context) Handler {
|
||||
return Handler{ctx: ctx}
|
||||
}
|
||||
|
||||
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.serveHTTP(w, r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
if strings.HasPrefix(r.URL.Path, "/v1/vpntor") {
|
||||
return h.vpntor(r.Context(), r.Body)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/v1/feeds") {
|
||||
return h.feeds(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/experimental/ui") || r.URL.Path == "/" {
|
||||
return h.ui(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/experimental/echo") {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
s := fmt.Sprintf("%s / %s", r.URL.String(), b)
|
||||
fmt.Fprintf(w, "%s\n", s)
|
||||
log.Printf("%s", b)
|
||||
return nil
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
36
src/cmd/server/handler/handler_test.go
Normal file
36
src/cmd/server/handler/handler_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"show-rss/src/cmd/server/handler"
|
||||
"show-rss/src/db"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
h := handler.New(db.Test(t, context.Background()))
|
||||
|
||||
{
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/not-found", nil)
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("%+v", w)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/experimental/echo", strings.NewReader("body"))
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("%+v", w)
|
||||
}
|
||||
if body := string(w.Body.Bytes()); body != "/experimental/echo / body\n" {
|
||||
t.Errorf("%s", body)
|
||||
}
|
||||
}
|
||||
}
|
||||
886
src/cmd/server/handler/public/dark.css
Normal file
886
src/cmd/server/handler/public/dark.css
Normal file
@@ -0,0 +1,886 @@
|
||||
/**
|
||||
* Forced dark theme version
|
||||
*/
|
||||
|
||||
:root {
|
||||
--background-body: #202b38;
|
||||
--background: #161f27;
|
||||
--background-alt: #1a242f;
|
||||
--selection: #1c76c5;
|
||||
--text-main: #dbdbdb;
|
||||
--text-bright: #fff;
|
||||
--text-muted: #a9b1ba;
|
||||
--links: #41adff;
|
||||
--focus: #0096bfab;
|
||||
--border: #526980;
|
||||
--code: #ffbe85;
|
||||
--animation-duration: 0.1s;
|
||||
--button-base: #0c151c;
|
||||
--button-hover: #040a0f;
|
||||
--scrollbar-thumb: var(--button-hover);
|
||||
--scrollbar-thumb-hover: rgb(0, 0, 0);
|
||||
--form-placeholder: #a9a9a9;
|
||||
--form-text: #fff;
|
||||
--variable: #d941e2;
|
||||
--highlight: #efdb43;
|
||||
--select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E");
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: #040a0f #202b38;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--background-body);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif;
|
||||
line-height: 1.4;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 0 10px;
|
||||
word-wrap: break-word;
|
||||
color: #dbdbdb;
|
||||
color: var(--text-main);
|
||||
background: #202b38;
|
||||
background: var(--background-body);
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
button {
|
||||
transition:
|
||||
background-color 0.1s linear,
|
||||
border-color 0.1s linear,
|
||||
color 0.1s linear,
|
||||
box-shadow 0.1s linear,
|
||||
transform 0.1s ease;
|
||||
transition:
|
||||
background-color var(--animation-duration) linear,
|
||||
border-color var(--animation-duration) linear,
|
||||
color var(--animation-duration) linear,
|
||||
box-shadow var(--animation-duration) linear,
|
||||
transform var(--animation-duration) ease;
|
||||
}
|
||||
|
||||
input {
|
||||
transition:
|
||||
background-color 0.1s linear,
|
||||
border-color 0.1s linear,
|
||||
color 0.1s linear,
|
||||
box-shadow 0.1s linear,
|
||||
transform 0.1s ease;
|
||||
transition:
|
||||
background-color var(--animation-duration) linear,
|
||||
border-color var(--animation-duration) linear,
|
||||
color var(--animation-duration) linear,
|
||||
box-shadow var(--animation-duration) linear,
|
||||
transform var(--animation-duration) ease;
|
||||
}
|
||||
|
||||
textarea {
|
||||
transition:
|
||||
background-color 0.1s linear,
|
||||
border-color 0.1s linear,
|
||||
color 0.1s linear,
|
||||
box-shadow 0.1s linear,
|
||||
transform 0.1s ease;
|
||||
transition:
|
||||
background-color var(--animation-duration) linear,
|
||||
border-color var(--animation-duration) linear,
|
||||
color var(--animation-duration) linear,
|
||||
box-shadow var(--animation-duration) linear,
|
||||
transform var(--animation-duration) ease;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2em;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-bottom: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #fff;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #fff;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #fff;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #fff;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: #fff;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h6 {
|
||||
color: #fff;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #fff;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
b,
|
||||
strong,
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
q::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
q::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #0096bfab;
|
||||
border-left: 4px solid var(--focus);
|
||||
margin: 1.5em 0;
|
||||
padding: 0.5em 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
q {
|
||||
border-left: 4px solid #0096bfab;
|
||||
border-left: 4px solid var(--focus);
|
||||
margin: 1.5em 0;
|
||||
padding: 0.5em 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote > footer {
|
||||
font-style: normal;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
blockquote cite {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
address {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
a[href^='mailto\:']::before {
|
||||
content: '📧 ';
|
||||
}
|
||||
|
||||
a[href^='tel\:']::before {
|
||||
content: '📞 ';
|
||||
}
|
||||
|
||||
a[href^='sms\:']::before {
|
||||
content: '💬 ';
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #efdb43;
|
||||
background-color: var(--highlight);
|
||||
border-radius: 2px;
|
||||
padding: 0 2px 0 2px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
a > code,
|
||||
a > strong {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select,
|
||||
input[type='submit'],
|
||||
input[type='reset'],
|
||||
input[type='button'],
|
||||
input[type='checkbox'],
|
||||
input[type='range'],
|
||||
input[type='radio'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[type='checkbox'],
|
||||
[type='radio'] {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
input {
|
||||
color: #fff;
|
||||
color: var(--form-text);
|
||||
background-color: #161f27;
|
||||
background-color: var(--background);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button {
|
||||
color: #fff;
|
||||
color: var(--form-text);
|
||||
background-color: #161f27;
|
||||
background-color: var(--background);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
color: #fff;
|
||||
color: var(--form-text);
|
||||
background-color: #161f27;
|
||||
background-color: var(--background);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
select {
|
||||
color: #fff;
|
||||
color: var(--form-text);
|
||||
background-color: #161f27;
|
||||
background-color: var(--background);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #0c151c;
|
||||
background-color: var(--button-base);
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
input[type='submit'] {
|
||||
background-color: #0c151c;
|
||||
background-color: var(--button-base);
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
input[type='reset'] {
|
||||
background-color: #0c151c;
|
||||
background-color: var(--button-base);
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
input[type='button'] {
|
||||
background-color: #0c151c;
|
||||
background-color: var(--button-base);
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #040a0f;
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
input[type='submit']:hover {
|
||||
background: #040a0f;
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
input[type='reset']:hover {
|
||||
background: #040a0f;
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
input[type='button']:hover {
|
||||
background: #040a0f;
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
input[type='color'] {
|
||||
min-height: 2rem;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
label {
|
||||
vertical-align: middle;
|
||||
margin-bottom: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input:not([type='checkbox']):not([type='radio']),
|
||||
input[type='range'],
|
||||
select,
|
||||
button,
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
display: block;
|
||||
margin-right: 0;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
textarea:not([cols]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea:not([rows]) {
|
||||
min-height: 40px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
select {
|
||||
background: #161f27 url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat;
|
||||
background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat;
|
||||
padding-right: 35px;
|
||||
}
|
||||
|
||||
select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
padding-right: 10px;
|
||||
background-image: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
box-shadow: 0 0 0 2px #0096bfab;
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
select:focus {
|
||||
box-shadow: 0 0 0 2px #0096bfab;
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
button:focus {
|
||||
box-shadow: 0 0 0 2px #0096bfab;
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
box-shadow: 0 0 0 2px #0096bfab;
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
input[type='checkbox']:active,
|
||||
input[type='radio']:active,
|
||||
input[type='submit']:active,
|
||||
input[type='reset']:active,
|
||||
input[type='button']:active,
|
||||
input[type='range']:active,
|
||||
button:active {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
input:disabled,
|
||||
select:disabled,
|
||||
button:disabled,
|
||||
textarea:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: #a9a9a9;
|
||||
color: var(--form-placeholder);
|
||||
}
|
||||
|
||||
:-ms-input-placeholder {
|
||||
color: #a9a9a9;
|
||||
color: var(--form-placeholder);
|
||||
}
|
||||
|
||||
::-ms-input-placeholder {
|
||||
color: #a9a9a9;
|
||||
color: var(--form-placeholder);
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: #a9a9a9;
|
||||
color: var(--form-placeholder);
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 1px #0096bfab solid;
|
||||
border: 1px var(--focus) solid;
|
||||
border-radius: 6px;
|
||||
margin: 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
margin: 10px 0;
|
||||
padding: 10px 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input[type='range']:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type='range']::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 9.5px;
|
||||
-webkit-transition: 0.2s;
|
||||
transition: 0.2s;
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input[type='range']::-webkit-slider-thumb {
|
||||
box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #526980;
|
||||
background: var(--border);
|
||||
-webkit-appearance: none;
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
input[type='range']:focus::-webkit-slider-runnable-track {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
input[type='range']::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 9.5px;
|
||||
-moz-transition: 0.2s;
|
||||
transition: 0.2s;
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input[type='range']::-moz-range-thumb {
|
||||
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #526980;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
input[type='range']::-ms-track {
|
||||
width: 100%;
|
||||
height: 9.5px;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-width: 16px 0;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
input[type='range']::-ms-fill-lower {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
border: 0.2px solid #010101;
|
||||
border-radius: 3px;
|
||||
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
}
|
||||
|
||||
input[type='range']::-ms-fill-upper {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
border: 0.2px solid #010101;
|
||||
border-radius: 3px;
|
||||
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
}
|
||||
|
||||
input[type='range']::-ms-thumb {
|
||||
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
border: 1px solid #000;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #526980;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
input[type='range']:focus::-ms-fill-lower {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
input[type='range']:focus::-ms-fill-upper {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #41adff;
|
||||
color: var(--links);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
color: #ffbe85;
|
||||
color: var(--code);
|
||||
padding: 2.5px 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
samp {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
color: #ffbe85;
|
||||
color: var(--code);
|
||||
padding: 2.5px 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
time {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
color: #ffbe85;
|
||||
color: var(--code);
|
||||
padding: 2.5px 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
padding: 10px;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
var {
|
||||
color: #d941e2;
|
||||
color: var(--variable);
|
||||
font-style: normal;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
border: 1px solid #526980;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
color: #dbdbdb;
|
||||
color: var(--text-main);
|
||||
padding: 2px 4px 2px 4px;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #526980;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
table caption {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
thead {
|
||||
border-bottom: 1px solid #526980;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
tfoot {
|
||||
border-top: 1px solid #526980;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #161f27;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) button {
|
||||
background-color: #1a242f;
|
||||
background-color: var(--background-alt);
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) button:hover {
|
||||
background-color: #202b38;
|
||||
background-color: var(--background-body);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #161f27;
|
||||
background: var(--background);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #040a0f;
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(0, 0, 0);
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background-color: #1c76c5;
|
||||
background-color: var(--selection);
|
||||
color: #fff;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #1c76c5;
|
||||
background-color: var(--selection);
|
||||
color: #fff;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background-color: #1a242f;
|
||||
background-color: var(--background-alt);
|
||||
padding: 10px 10px 0;
|
||||
margin: 1em 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
details[open] {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
details > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
details[open] summary {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
background-color: #161f27;
|
||||
background-color: var(--background);
|
||||
padding: 10px;
|
||||
margin: -10px -10px 0;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
summary:hover,
|
||||
summary:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
details > :not(summary) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
color: #dbdbdb;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
dialog {
|
||||
background-color: #1a242f;
|
||||
background-color: var(--background-alt);
|
||||
color: #dbdbdb;
|
||||
color: var(--text-main);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
border-color: #526980;
|
||||
border-color: var(--border);
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
dialog > header:first-child {
|
||||
background-color: #161f27;
|
||||
background-color: var(--background);
|
||||
border-radius: 6px 6px 0 0;
|
||||
margin: -10px -30px 10px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
dialog::-webkit-backdrop {
|
||||
background: #0000009c;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: #0000009c;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 1px solid #526980;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 10px;
|
||||
color: #a9b1ba;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
body > footer {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body,
|
||||
pre,
|
||||
code,
|
||||
summary,
|
||||
details,
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre,
|
||||
code,
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
footer,
|
||||
summary,
|
||||
strong {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00f;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
56
src/cmd/server/handler/public/index.tmpl
Normal file
56
src/cmd/server/handler/public/index.tmpl
Normal file
@@ -0,0 +1,56 @@
|
||||
<html>
|
||||
<header>
|
||||
<link rel="stylesheet" href="/experimental/ui/dark.css">
|
||||
</header>
|
||||
<body>
|
||||
<h2><a href="?">Feeds</a></h2>
|
||||
|
||||
<div>
|
||||
{{ range feeds }}
|
||||
<div>
|
||||
<h3><code><a href="?edit={{.Entry.ID}}">{{ .Version.URL }}</a></code></h3>
|
||||
<div>@<code>{{ .Version.Cron }}</code> ~<code>{{ .Version.Pattern }}</code></div>
|
||||
<div><code>{{ .Version.WebhookMethod }} {{ .Version.WebhookURL }} | {{ .Version.WebhookBody }}</code></div>
|
||||
<div>(last run {{ ago .Execution.Executed }} ago)</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<br><hr><br>
|
||||
|
||||
<div>
|
||||
<h3>
|
||||
{{ if eq "" .editing.ID }}
|
||||
New
|
||||
{{ else }}
|
||||
Updating <code><a target="_blank" href="{{ .editing_url }}">{{ .editing.URL }}</a></code> (<a href="?">clear</a>)
|
||||
<br>
|
||||
<div style="scale: 0.85">
|
||||
<form method="POST" action="/v1/feeds?id={{ .editing.ID }}&delete">
|
||||
<button type="submit">DELETE</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
</h3>
|
||||
<form method="POST" action="/v1/feeds?id={{ .editing.ID }}">
|
||||
{{ range $k, $v := .editing }}
|
||||
{{ if not (in $k "Created" "Deleted" "Updated" "ID") }}
|
||||
<div>
|
||||
<label for="{{ $k }}">
|
||||
{{ $k }}
|
||||
{{- if eq $k "URL" }}
|
||||
(hint: nyaa://?q=show)
|
||||
{{ else if eq $k "WebhookURL" }}
|
||||
(hint: vpntor:///outdir)
|
||||
{{ end }}
|
||||
</label>
|
||||
<input name="{{ $k }}" type="text" value="{{ $v }}"/>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<button type="submit">Submit</button>
|
||||
<code>Preview someday</code>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
491
src/cmd/server/handler/testdata/feed.rss
vendored
Normal file
491
src/cmd/server/handler/testdata/feed.rss
vendored
Normal file
@@ -0,0 +1,491 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
|
||||
<rdf:RDF
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://purl.org/rss/1.0/"
|
||||
xmlns:admin="http://webns.net/mvcb/"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
|
||||
xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"
|
||||
>
|
||||
|
||||
<channel rdf:about="https://slashdot.org/">
|
||||
<title>Slashdot</title>
|
||||
<link>https://slashdot.org/</link>
|
||||
<description>News for nerds, stuff that matters</description>
|
||||
<dc:language>en-us</dc:language>
|
||||
<dc:rights>Copyright Slashdot Media. All Rights Reserved.</dc:rights>
|
||||
<dc:date>2025-04-27T18:06:25+00:00</dc:date>
|
||||
<dc:publisher>Slashdot Media</dc:publisher>
|
||||
<dc:creator>feedback@slashdot.org</dc:creator>
|
||||
<dc:subject>Technology</dc:subject>
|
||||
<syn:updateBase>1970-01-01T00:00+00:00</syn:updateBase>
|
||||
<syn:updateFrequency>1</syn:updateFrequency>
|
||||
<syn:updatePeriod>hourly</syn:updatePeriod>
|
||||
<items>
|
||||
<rdf:Seq>
|
||||
<rdf:li rdf:resource="https://it.slashdot.org/story/25/04/27/088238/wsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://news.slashdot.org/story/25/04/26/238243/canadian-university-cancels-coding-competition-over-suspected-ai-cheating?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://linux.slashdot.org/story/25/04/27/0127203/lenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://entertainment.slashdot.org/story/25/04/27/040248/yoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://linux.slashdot.org/story/25/04/27/0547245/linus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://tech.slashdot.org/story/25/04/27/0252257/4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://tech.slashdot.org/story/25/04/27/0031222/ipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://science.slashdot.org/story/25/04/26/2217249/can-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://it.slashdot.org/story/25/04/26/2042230/read-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://entertainment.slashdot.org/story/25/04/26/1935238/youtube-is-huge---and-a-few-creators-are-getting-rich?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://mobile.slashdot.org/story/25/04/26/078214/can-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://news.slashdot.org/story/25/04/26/0625244/california-becomes-the-worlds-fourth-largest-economy-overtaking-japan?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://news.slashdot.org/story/25/04/26/0520221/us-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://slashdot.org/story/25/04/26/0742205/nyt-asks-should-we-start-taking-the-welfare-of-ai-seriously?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
</rdf:Seq>
|
||||
</items>
|
||||
<image rdf:resource="https://a.fsdn.com/sd/topics/topicslashdot.gif" />
|
||||
<textinput rdf:resource="https://slashdot.org/search.pl" />
|
||||
</channel>
|
||||
<image rdf:about="https://a.fsdn.com/sd/topics/topicslashdot.gif">
|
||||
<title>Slashdot</title>
|
||||
<url>https://a.fsdn.com/sd/topics/topicslashdot.gif</url>
|
||||
<link>https://slashdot.org/</link>
|
||||
</image>
|
||||
<item rdf:about="https://it.slashdot.org/story/25/04/27/088238/wsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>WSJ: Tech-Industry Workers Now 'Miserable', Fearing Layoffs, Working Longer Hours</title>
|
||||
<link>https://it.slashdot.org/story/25/04/27/088238/wsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"Not so long ago, working in tech meant job security, extravagant perks and a bring-your-whole-self-to-the-office ethos rare in other industries," writes the Wall Street Journal.
|
||||
|
||||
But now tech work "looks like a regular job," with workers "contending with the constant fear of layoffs, longer hours and an ever-growing list of responsibilities for the same pay."
|
||||
|
||||
Now employees find themselves doing the work of multiple laid-off colleagues. Some have lost jobs only to be rehired into positions that aren't eligible for raises or stock grants. Changing jobs used to be a surefire way to secure a raise; these days, asking for more money can lead to a job offer being withdrawn.
|
||||
|
||||
The shift in tech has been building slowly. For years, demand for workers outstripped supply, a dynamic that peaked during the Covid-19 pandemic. Big tech companies like Meta and Salesforce admitted they brought on too many employees. The ensuing downturn included mass layoffs that started in 2022...
|
||||
|
||||
[S]ome longtime tech employees say they no longer recognize the companies they work for. Management has become more focused on delivering the results Wall Street expects. Revenue remains strong for tech giants, but they're pouring resources into costly AI infrastructure, putting pressure on cash flow. With the industry all grown up, a heads-down, keep-quiet mentality has taken root, workers say... Tech workers are still well-paid compared with other sectors, but currently there's a split in the industry. Those working in AI &mdash; and especially those with Ph.D.s &mdash; are seeing their compensation packages soar. But those without AI experience are finding they're better off staying where they are, because companies aren't paying what they were a few years ago.
|
||||
|
||||
Other excepts from the Wall Street Journal's article:
|
||||
|
||||
"I'm hearing of people having 30 direct reports," says David Markley, who spent seven years at Amazon and is now an executive coach for workers at large tech companies. "It's not because the companies don't have the money. In a lot of ways, it's because of AI and the narratives out there about how collapsing the organization is better...."
|
||||
In some cases, companies post record revenue while still trimming head count.
|
||||
Google co-founder Sergey Brin told a group of employees in February that 60 hours a week was the sweet spot of productivity, in comments reported earlier by the New York Times.
|
||||
One recruiter at Meta who had been laid off by the company was rehired into her old role last year, but with a catch: She's now classified as a "short-term employee." Her contract is eligible for renewal, but she doesn't get merit pay increases, promotions or stock. The recruiter says she's responsible for a volume of work that used to be spread among several people. The company refers to being loaded with such additional responsibilities as "agility." More than 50,000 tech workers from over 100 companies have been laid off in 2025, according to Layoffs.fyi, a website that tracks job cuts and crowdsources lists of laid off workers...
|
||||
Even before those 50,000 layoffs in 2025,
|
||||
Silicon Valley's Mercury News was citing some interesting statistics from economic research/consulting firm Beacon Economics. In 2020, 2021 and 2022, the San Francisco Bay Area added 74,700 tech jobs But then in 2023 and 2024 the industry had slashed even more tech jobs -- 80,200 -- for a net loss (over five years) of 5,500.
|
||||
|
||||
So is there really a cutback in perks and a fear of layoffs that's casting a pall over the industry? share your own thoughts and experiences in the comments. Do you agree with the picture that's being painted by the Wall Street Journal?
|
||||
|
||||
They told their readers that tech workers are now "just like the rest of us: miserable at work."<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=WSJ%3A++Tech-Industry+Workers+Now+'Miserable'%2C+Fearing+Layoffs%2C+Working+Longer+Hours%3A+https%3A%2F%2Fit.slashdot.org%2Fstory%2F25%2F04%2F27%2F088238%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fit.slashdot.org%2Fstory%2F25%2F04%2F27%2F088238%2Fwsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://it.slashdot.org/story/25/04/27/088238/wsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676561&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
<dc:subject>it</dc:subject>
|
||||
<slash:department>misery-loves-company</slash:department>
|
||||
<slash:section>it</slash:section>
|
||||
<slash:comments>22</slash:comments>
|
||||
<slash:hit_parade>22,22,18,16,3,2,1</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://news.slashdot.org/story/25/04/26/238243/canadian-university-cancels-coding-competition-over-suspected-ai-cheating?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Canadian University Cancels Coding Competition Over Suspected AI Cheating</title>
|
||||
<link>https://news.slashdot.org/story/25/04/26/238243/canadian-university-cancels-coding-competition-over-suspected-ai-cheating?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>The university blamed it on "the significant number of students" who violated their coding competition's rules.
|
||||
Long-time Slashdot reader theodp quotes this report from The Logic: Finding that many students violated rules and submitted code not written by themselves, the University of Waterloo's Centre for Computing and Math decided not to release results from its annual Canadian Computing Competition (CCC), which many students rely on to bolster their chances of being accepted into Waterloo's prestigious computing and engineering programs, or land a spot on teams to represent Canada in international competitions. "It is clear that many students submitted code that they did not write themselves, relying instead on forbidden external help," the CCC co-chairs explained in a statement. "As such, the reliability of 'ranking' students would neither be equitable, fair, or accurate."
|
||||
|
||||
"It is disappointing that the students who violated the CCC Rules will impact those students who are deserving of recognition," the univeresity said in its statement. They added that they are "considering possible ways to address this problem for future contests."<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Canadian+University+Cancels+Coding+Competition+Over+Suspected+AI+Cheating%3A+https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F238243%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F238243%2Fcanadian-university-cancels-coding-competition-over-suspected-ai-cheating%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://news.slashdot.org/story/25/04/26/238243/canadian-university-cancels-coding-competition-over-suspected-ai-cheating?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676353&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T15:34:00+00:00</dc:date>
|
||||
<dc:subject>education</dc:subject>
|
||||
<slash:department>halt-and-catch-fire</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>10</slash:comments>
|
||||
<slash:hit_parade>10,10,9,8,2,1,0</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://linux.slashdot.org/story/25/04/27/0127203/lenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Lenovo May Be Avoiding the 'Windows Tax' By Offering Cheaper Laptops With Pre-Installed Linux</title>
|
||||
<link>https://linux.slashdot.org/story/25/04/27/0127203/lenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"The U.S. and Canadian websites for Lenovo offered U.S. $140 and CAD $211 off on the same ThinkPad X1 Carbon model when choosing any one of the Linux-based alternatives," reports It's FOSS News:
|
||||
|
||||
|
||||
This was brought to my attention thanks to a Reddit post... Others then chimed in, saying that Lenovo has been doing this since at least 2020 and that the big price difference shows how ridiculous Windows' pricing is...
|
||||
Not all models from their laptop lineup, like ThinkPad, Yoga, Legion, LOQ, etc., feature an option to get Linux pre-installed during the checkout process. Luckily, there is an easy way to filter through the numerous laptops. Just go to the laptops section (U.S.) on the Lenovo website and turn on the "Operating System" filter under the Filter by specs sidebar menu.
|
||||
|
||||
The article end with an embedded YouTube video showing a VCR playing a videotape of a 1999 local TV news report... about the legendary "Windows Refund Day" protests.
|
||||
|
||||
Slashdot ran numerous stories about the event &mdash; including one by Jon Katz...<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Lenovo+May+Be+Avoiding+the+'Windows+Tax'+By+Offering+Cheaper+Laptops+With+Pre-Installed+Linux%3A+https%3A%2F%2Flinux.slashdot.org%2Fstory%2F25%2F04%2F27%2F0127203%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Flinux.slashdot.org%2Fstory%2F25%2F04%2F27%2F0127203%2Flenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://linux.slashdot.org/story/25/04/27/0127203/lenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676403&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T14:34:00+00:00</dc:date>
|
||||
<dc:subject>portables</dc:subject>
|
||||
<slash:department>Windows-refund-day</slash:department>
|
||||
<slash:section>linux</slash:section>
|
||||
<slash:comments>23</slash:comments>
|
||||
<slash:hit_parade>23,23,22,19,3,1,1</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://entertainment.slashdot.org/story/25/04/27/040248/yoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Yoda Bloopers Released - and George Lucas Reveals Why Yoda Talks Backwards</title>
|
||||
<link>https://entertainment.slashdot.org/story/25/04/27/040248/yoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>80-year-old George Lucas appeared this week at a 45th anniversary screening of The Empire Strikes Back, reports CNN &mdash; and finally gave a good explanation for why Yoda speaks the way he does. "He explained that it came about in order to ensure that the little alien's usually profound messages really landed with audiences."
|
||||
"Because if you speak regular English, people won't listen that much," Lucas said at the 2025 TCM Classic Film Festival, per Variety . "But if he had an accent, or it's really hard to understand what he's saying, they focus on what he's saying." Yoda was "basically the philosopher of the movie," the filmmaker added. "I had to figure out a way to get people to actually listen &mdash; especially 12-year-olds."
|
||||
|
||||
Also this week, the verified Instagram accounts for Disney+, Star Wars and LucasFilm &mdash; Lucas' film and television production company &mdash; posted clips of Yoda doing bloopers on the set of "Star Wars" films, with [Frank] Oz continuing to do the voice and manipulate the heavy Yoda puppet even on takes that were unusable. Suffice it to say: One for the ages, Yoda is.
|
||||
|
||||
Lucas also remembered how he'd "mounted a guerilla campaign to generate excitement" for the first Star Wars movie, reports Variety. ("I got the kids walking around Disneyland and the Comic Cons and all that kind of stuff... that's why Fox was so shocked when the first day the lines were all around the block.") And Variety says Lucas described a condition in his contract for Star Wars "that would again be life-changing, both for him and the entertainment industry as a whole."
|
||||
|
||||
"I said, 'besides that, I'd like licensing.' They went, 'What's licensing?'" Unimpressed by the film, and colored by the history of movie merchandising to that point, the studio capitulated to his demands. "They talked to themselves, and they went, 'He's never going to be able to do that. It takes them a billion dollars and a year to make a toy or make anything. There's no money in that at all.'"<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Yoda+Bloopers+Released+-+and+George+Lucas+Reveals+Why+Yoda+Talks+Backwards%3A+https%3A%2F%2Fentertainment.slashdot.org%2Fstory%2F25%2F04%2F27%2F040248%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fentertainment.slashdot.org%2Fstory%2F25%2F04%2F27%2F040248%2Fyoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://entertainment.slashdot.org/story/25/04/27/040248/yoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676461&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T11:34:00+00:00</dc:date>
|
||||
<dc:subject>starwars</dc:subject>
|
||||
<slash:department>do-or-do-not-do</slash:department>
|
||||
<slash:section>entertainment</slash:section>
|
||||
<slash:comments>19</slash:comments>
|
||||
<slash:hit_parade>19,19,14,13,1,0,0</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://linux.slashdot.org/story/25/04/27/0547245/linus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Linus Torvalds Expresses His Hatred For Case-Insensitive File-Systems</title>
|
||||
<link>https://linux.slashdot.org/story/25/04/27/0547245/linus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>Some patches for Linux 6.15-rc4 (updating the kernel driver for the Bcachefs file system) triggered some "straight-to-the-point wisdom" from Linus Torvalds about case-insensitive filesystems, reports Phoronix.
|
||||
|
||||
Bcachefs developer Kent Overstreet started the conversation, explaining how some buggy patches for their case-insensitive file and folder support were upstreamed into the Bcachefs kernel driver nearly two years ago:
|
||||
|
||||
When I was discussing with the developer who did the implementation, I noted that fstests should already have tests. However, it seems I neglected to tell him to make sure the tests actually run... It is _not_ enough to simply rely on the automated tests. You have to have eyes on what your code is doing.
|
||||
Overstreet added "There's a story behind the case insensitive directory fixes, and lessons to be learned." To which Torvalds replied.... "No."
|
||||
"The only lesson to be learned is that filesystem people never learn."
|
||||
|
||||
|
||||
|
||||
Torvalds: Case-insensitive names are horribly wrong, and you shouldn't have done them at all. The problem wasn't the lack of testing, the problem was implementing it in the first place. The problem is then compounded by "trying to do it right", and in the process doing it horrible wrong indeed, because "right" doesn't exist, but trying to will make random bytes have very magical meaning.
|
||||
|
||||
And btw, the tests are all completely broken anyway. Last I saw, they didn't actually test for all the really interesting cases &mdash; the ones that cause security issues in user land. Security issues like "user space checked that the filename didn't match some security-sensitive pattern". And then the shit-for-brains filesystem ends up matching that pattern *anyway*, because the people who do case insensitivity *INVARIABLY* do things like ignore non-printing characters, so now "case insensitive" also means "insensitive to other things too"....
|
||||
Dammit. Case sensitivity is a BUG. The fact that filesystem people *still* think it's a feature, I cannot understand. It's like they revere the old FAT filesystem _so_ much that they have to recreate it &mdash; badly.
|
||||
|
||||
And this led to a very lively back-and-forth discussion.
|
||||
|
||||
Slashdot's summary of the highlights:<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Linus+Torvalds+Expresses+His+Hatred+For+Case-Insensitive+File-Systems%3A+https%3A%2F%2Flinux.slashdot.org%2Fstory%2F25%2F04%2F27%2F0547245%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Flinux.slashdot.org%2Fstory%2F25%2F04%2F27%2F0547245%2Flinus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://linux.slashdot.org/story/25/04/27/0547245/linus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676497&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T07:34:00+00:00</dc:date>
|
||||
<dc:subject>linux</dc:subject>
|
||||
<slash:department>insensitivity-training</slash:department>
|
||||
<slash:section>linux</slash:section>
|
||||
<slash:comments>159</slash:comments>
|
||||
<slash:hit_parade>159,159,155,138,33,21,14</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://tech.slashdot.org/story/25/04/27/0252257/4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>4chan Returns, Details Breach, Blames Funding Issues, Ends Shockwave Board</title>
|
||||
<link>https://tech.slashdot.org/story/25/04/27/0252257/4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"4chan, down for more than a week after hackers got in through an insecure script that handled PDFs, is back online," notes BoingBoing. (They add that Thursday saw 4chan's first blog postin years &mdash; just the words "Testing testing 123 123...") But 4chan posted a much longer explanation on Friday," confirming their servers were compromised by a malicious PDF upload from "a hacker using a UK IP address," granting access to their databases and administrative dashboard.
|
||||
|
||||
|
||||
The attacker "spent several hours exfiltrating database tables and much of 4chan's source code. When they had finished downloading what they wanted, they began to vandalize 4chan at which point moderators became aware and 4chan's servers were halted, preventing further access."
|
||||
|
||||
While not all of our servers were breached, the most important one was, and it was due to simply not updating old operating systems and code in a timely fashion. Ultimately this problem was caused by having insufficient skilled man-hours available to update our code and infrastructure, and being starved of money for years by advertisers, payment providers, and service providers who had succumbed to external pressure campaigns. We had begun a process of speccing new servers in late 2023. As many have suspected, until that time 4chan had been running on a set of servers purchased second-hand by moot a few weeks before his final Q&amp;A [in 2015], as prior to then we simply were not in a financial position to consider such a large purchase. Advertisers and payment providers willing to work with 4chan are rare, and are quickly pressured by activists into cancelling their services. Putting together the money for new equipment took nearly a decade...
|
||||
|
||||
The free time that 4chan's development team had available to dedicate to 4chan was insufficient to update our software and infrastructure fast enough, and our luck ran out. However, we have not been idle during our nearly two weeks of downtime. The server that was breached has been replaced, with the operating system and code updated to the latest versions. PDF uploads have been temporarily disabled on those boards that supported them, but they will be back in the near future. One slow but much beloved board, /f/ &mdash; Flash, will not be returning however, as there is no realistic way to prevent similar exploits using .swf files.
|
||||
|
||||
We are bringing on additional volunteer developers to help keep up with the workload, and our team of volunteer janitors &amp; moderators remains united despite the grievous violations some have suffered to their personal privacy.
|
||||
4chan is back. No other website can replace it, or this community. No matter how hard it is, we are not giving up.
|
||||
|
||||
<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=4chan+Returns%2C+Details+Breach%2C+Blames+Funding+Issues%2C+Ends+Shockwave+Board%3A+https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F27%2F0252257%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F27%2F0252257%2F4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://tech.slashdot.org/story/25/04/27/0252257/4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676441&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T04:34:00+00:00</dc:date>
|
||||
<dc:subject>social</dc:subject>
|
||||
<slash:department>artistic-works-of-fiction</slash:department>
|
||||
<slash:section>technology</slash:section>
|
||||
<slash:comments>40</slash:comments>
|
||||
<slash:hit_parade>40,35,30,24,7,3,1</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://tech.slashdot.org/story/25/04/27/0031222/ipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>iPad Jammed in Seat Forces Emergency Landing of Airplane Carrying 400 Passengers</title>
|
||||
<link>https://tech.slashdot.org/story/25/04/27/0031222/ipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>An anonymous reader shared this report from Business Insider:
|
||||
|
||||
A Lufthansa flight carrying 461 passengers had to divert after someone's tablet became "jammed" in a business-class seat.
|
||||
|
||||
The Airbus A380 took off from Los Angeles on Wednesday, bound for Munich, and had been flying for around three hours when the pilots diverted to Boston Logan International Airport. In a statement to Business Insider, an airline spokesperson said the tablet had become "jammed in a Business Class seat" and had "already shown visible signs of deformation due to the seat's movements" when the flight diverted. [The aviation site] Simply Flying, which first reported the news, said the device was an iPad.
|
||||
|
||||
The decision to divert was taken "to eliminate any potential risk, particularly with regard to possible overheating," the spokesperson added, saying that it was the joint decision of the crew and air traffic control. Lithium batteries pose a safety risk if damaged, punctured, or crushed... In a confined space like an aircraft cabin, a lithium battery fire poses a serious hazard to the passengers onboard. Last year, a Breeze Airways flight from Los Angeles to Pittsburgh had to make an emergency landing in Albuquerque after a passenger's laptop caught fire.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=iPad+Jammed+in+Seat+Forces+Emergency+Landing+of+Airplane+Carrying+400+Passengers%3A+https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F27%2F0031222%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F27%2F0031222%2Fipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://tech.slashdot.org/story/25/04/27/0031222/ipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676377&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T01:34:00+00:00</dc:date>
|
||||
<dc:subject>transportation</dc:subject>
|
||||
<slash:department>battery-low</slash:department>
|
||||
<slash:section>technology</slash:section>
|
||||
<slash:comments>56</slash:comments>
|
||||
<slash:hit_parade>56,53,45,38,9,3,2</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://science.slashdot.org/story/25/04/26/2217249/can-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Can Solar Wind Make Water on the Moon? A NASA Experiment Shows Maybe </title>
|
||||
<link>https://science.slashdot.org/story/25/04/26/2217249/can-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"Future moon astronauts may find water more accessible than previously thought," writes Space.com, citing a new NASA-led experiment:
|
||||
|
||||
Because the moon lacks a magnetic field like Earth's, the barren lunar surface is constantly bombarded by energetic particles from the sun... Li Hsia Yeo, a planetary scientist at NASA's Goddard Space Flight Center in Maryland, led a lab experiment observing the effects of simulated solar wind on two samples of loose regolith brought to Earth by the Apollo 17 mission... To mimic conditions on the moon, the researchers built a custom apparatus that included a vacuum chamber, where the samples were placed, and a tiny particle accelerator, which the scientists used to bombard the samples with hydrogen ions for several days.
|
||||
|
||||
"The exciting thing here is that with only lunar soil and a basic ingredient from the sun &mdash; which is always spitting out hydrogen &mdash; there's a possibility of creating water," Yeo said in a statement. "That's incredible to think about." Supporting this idea, observations from previous moon missions have revealed an abundance of hydrogen gas in the moon's tenuous atmosphere. Scientists suspect that solar-wind-driven heating facilitates the combination of hydrogen atoms on the surface into hydrogen gas, which then escapes into space. This process also has a surprising upside, the new study suggests. Leftover oxygen atoms are free to bond with new hydrogen atoms formed by repeated bombardment of the solar wind, prepping the moon for more water formation on a renewable basis.
|
||||
|
||||
The findings could help assess how sustainable water on the moon is, as the sought-after resource is crucial for both life support and as propellant for rockets. The team's study was published in March in the journal JGR Planets .
|
||||
|
||||
NASA created a fascinating animation showing how water is released from the Moon during meteor showers. (In 2016 scientists discovered that when speck of comet debris vaporize on impact, they create shock waves in the lunar soil which can sometimes breach the dry upper layer, releasing water molecules from the hydrated layer below...)<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Can+Solar+Wind+Make+Water+on+the+Moon%3F+A+NASA+Experiment+Shows+Maybe+%3A+https%3A%2F%2Fscience.slashdot.org%2Fstory%2F25%2F04%2F26%2F2217249%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fscience.slashdot.org%2Fstory%2F25%2F04%2F26%2F2217249%2Fcan-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://science.slashdot.org/story/25/04/26/2217249/can-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676331&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T22:34:00+00:00</dc:date>
|
||||
<dc:subject>moon</dc:subject>
|
||||
<slash:department>moonshining</slash:department>
|
||||
<slash:section>science</slash:section>
|
||||
<slash:comments>19</slash:comments>
|
||||
<slash:hit_parade>19,17,15,13,4,4,3</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://it.slashdot.org/story/25/04/26/2042230/read-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>'Read the Manual': Misconfigured Google Analytics Led to a Data Breach Affecting 4.7M</title>
|
||||
<link>https://it.slashdot.org/story/25/04/26/2042230/read-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>Slashdot reader itwbennett writes: Personal health information on 4.7 million Blue Shield California subscribers was unintentionally shared between Google Analytics and Google Ads between April 2021 and January 2025 due to a misconfiguration error. Security consultant and SANS Institute instructor Brandon Evans points to two lessons to take from this debacle:
|
||||
|
||||
Read the documentation of any third party service you sign up for, to understand the security and privacy controls;Know what data is being collected from your organization, and what you don't want shared.
|
||||
|
||||
"If there is a concern by the organization that Google Ads would use this information, they should really consider whether or not they should be using a platform like Google Analytics in the first place," Evans says in the article. "Because from a technical perspective, there is nothing stopping Google from sharing the information across its platform...
|
||||
|
||||
"Google definitely gives you a great bunch of controls, but technically speaking, that data is within the walls of that organization, and it's impossible to know from the outside how that data is being used."<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status='Read+the+Manual'%3A+Misconfigured+Google+Analytics+Led+to+a+Data+Breach+Affecting+4.7M%3A+https%3A%2F%2Fit.slashdot.org%2Fstory%2F25%2F04%2F26%2F2042230%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fit.slashdot.org%2Fstory%2F25%2F04%2F26%2F2042230%2Fread-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://it.slashdot.org/story/25/04/26/2042230/read-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676287&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T21:34:00+00:00</dc:date>
|
||||
<dc:subject>google</dc:subject>
|
||||
<slash:department>feeling-insecure</slash:department>
|
||||
<slash:section>it</slash:section>
|
||||
<slash:comments>14</slash:comments>
|
||||
<slash:hit_parade>14,14,10,9,3,2,2</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://entertainment.slashdot.org/story/25/04/26/1935238/youtube-is-huge---and-a-few-creators-are-getting-rich?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>YouTube is Huge - and a Few Creators Are Getting Rich</title>
|
||||
<link>https://entertainment.slashdot.org/story/25/04/26/1935238/youtube-is-huge---and-a-few-creators-are-getting-rich?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"Google-owned YouTube's revenue last year was estimated to be $54.2 billion," reports the Los Angeles Times, "which would make it the second-largest media company behind Walt Disney Co., according to a recent report from research firm MoffettNathanson, which called YouTube 'the new king of all media.'"
|
||||
|
||||
YouTube, run by Chief Executive Neal Mohan since 2023, accounted for 12% of U.S. TV viewing in March, more than other rival streaming platforms including Netflix and Tubi, according to Nielsen... More people are watching YouTube on TV sets rather than on smartphones and computer screens, consuming more than 1 billion hours on average of YouTube content on TV daily, the company said on its website.
|
||||
|
||||
When YouTube first started its founders envisioned it as a dating site, according to the article, "where people would upload videos and score them. When that didn't work, the founders decided to open up the platform for all sorts of videos." And since this was 20 years ago, "Users drove traffic to YouTube by sharing videos on MySpace."
|
||||
|
||||
But the article includes stories of people getting rich through YouTube's sharing of ad revenue:
|
||||
|
||||
Patrick Starrr, who produces makeup tutorial videos, said he made his first $1 million through YouTube at the age of 25. He left his job at retailer MAC Cosmetics in Florida and moved to L.A...
|
||||
|
||||
[Video creator Dhar Mann] started posting videos on YouTube in 2018 with no film background. Mann previously had a business that sold supplies to grow weed. Today, his company, Burbank-based Dhar Mann Studios, operates on 125,000 square feet of production space, employs roughly 200 people and works with 2,000 actors a year on family friendly programs that touch on how students and families deal with topics such as bullying, narcolepsy, chronic inflammatory bowel disease and hoarding. Mann made $45 million last year, according to Forbes estimates. The majority of his company's revenue comes through YouTube.
|
||||
He tells the Times "I don't think it's just the future of TV &mdash; it is TV, and the world is catching on."
|
||||
|
||||
And then there's this...
|
||||
|
||||
"My mom would always give me so much crap about it &mdash; she would say, 'Why do you want to do YouTube?'" said Chucky Appleby, now an executive at MrBeast. His reply: "Mom, you can make a living from this." MrBeast's holding company, Beast Industries, which employs more than 400 people, made $473 million in revenue last year, according to Business Insider. In the last 28 days, MrBeast content &mdash; which includes challenges and stunt videos &mdash; received 3.6 billion views on YouTube, Appleby said.
|
||||
|
||||
Appleby, 28, said he's since bought a Jeep for his mom.
|
||||
<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=YouTube+is+Huge+-+and+a+Few+Creators+Are+Getting+Rich%3A+https%3A%2F%2Fentertainment.slashdot.org%2Fstory%2F25%2F04%2F26%2F1935238%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fentertainment.slashdot.org%2Fstory%2F25%2F04%2F26%2F1935238%2Fyoutube-is-huge---and-a-few-creators-are-getting-rich%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://entertainment.slashdot.org/story/25/04/26/1935238/youtube-is-huge---and-a-few-creators-are-getting-rich?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676255&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T20:34:00+00:00</dc:date>
|
||||
<dc:subject>tv</dc:subject>
|
||||
<slash:department>video-killed-the-radio-star</slash:department>
|
||||
<slash:section>entertainment</slash:section>
|
||||
<slash:comments>27</slash:comments>
|
||||
<slash:hit_parade>27,26,22,19,9,5,3</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://mobile.slashdot.org/story/25/04/26/078214/can-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Can a New 'Dumbphone' With an E Ink Display Help Rewire Your Brain?</title>
|
||||
<link>https://mobile.slashdot.org/story/25/04/26/078214/can-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>ZDNet's reviewer says "I tested this affordable E Ink phone for two weeks, and it rewired my brain (for the better)."
|
||||
|
||||
It's Mudita's new Kompakt smartphone with a two-color E Ink display &mdash; which ZDNet calls "an affordable choice" for those "considering investing in a so-called 'dumbphone'..."
|
||||
|
||||
|
||||
Compared to modern smartphones, the Mudita Kompakt is a bit chunky at half an inch thick and five inches long. It's still rather light, though, weighing just 164 grams and covered in soft touch material, so it feels good in the hand. The bezels around the 4.3-inch display are rather large, with three touch-sensitive buttons for back, home, and quick settings, so navigating to key elements is intuitive, whether you're coming from Android or iOS.
|
||||
The phone features a fingerprint sensor to lock and unlock, and it's housed on the power button in the middle of the right side. I'm a huge fan of consolidating these two purposes to the same button, and it works flawlessly.... You can charge via the USB-C, but surprisingly, it also supports wireless charging. All in all, the battery is quite good. Mudita says it can last for up to six days on standby, with around two days of standard use. In my testing, I found this to be about accurate.
|
||||
|
||||
On the left side of the device is a button that houses one of its key features: offline mode. Switching to this mode disables all wireless connectivity and support for the camera, so it truly becomes distraction-free.. [T]here is undoubtedly some lag in certain apps &mdash; such as the camera &mdash; due to the E Ink display technology and processor/RAM specifications. You will also likely notice some lag in text messaging if you tap quickly on the keyboard, often resulting in getting ahead of the spell-checking feature. As far as apps go, in addition to phone calls and text messages, the Kompakt includes an alarm, calculator, chess game, maps, meditation, weather, and a voice recorder.
|
||||
|
||||
Phone calls "sounded great on both ends," according to the review. (And text messaging "works well if you don't tap too quickly on the keyboard.") But the 8MP camera produced photos "that look like they were taken over ten years ago." (And accessing the internal storage "requires connecting to a Windows PC and launching File Explorer," although "you can also just share photos via text messaging, as it's much faster than using a computer.") But ZDNet calls it an "attractive &mdash; if very simplified &mdash; E Ink display."
|
||||
|
||||
Mudita is asking $369 now for preorder customers, according to the article, while the phone will be available in May for $439.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Can+a+New+'Dumbphone'+With+an+E+Ink+Display+Help+Rewire+Your+Brain%3F%3A+https%3A%2F%2Fmobile.slashdot.org%2Fstory%2F25%2F04%2F26%2F078214%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fmobile.slashdot.org%2Fstory%2F25%2F04%2F26%2F078214%2Fcan-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://mobile.slashdot.org/story/25/04/26/078214/can-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675857&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T19:34:00+00:00</dc:date>
|
||||
<dc:subject>cellphones</dc:subject>
|
||||
<slash:department>phoning-it-in</slash:department>
|
||||
<slash:section>mobile</slash:section>
|
||||
<slash:comments>85</slash:comments>
|
||||
<slash:hit_parade>85,84,71,65,11,5,1</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://news.slashdot.org/story/25/04/26/0625244/california-becomes-the-worlds-fourth-largest-economy-overtaking-japan?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>California Becomes the World's Fourth-Largest Economy, Overtaking Japan</title>
|
||||
<link>https://news.slashdot.org/story/25/04/26/0625244/california-becomes-the-worlds-fourth-largest-economy-overtaking-japan?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"Only the United States, China and Germany have larger economies than California," reports CNN.
|
||||
|
||||
In fact, they add that California "outpaced all three countries with growth of 6% last year," according to the California governor's office (which cites new data from the International Monetary Fund and the U.S. Bureau of Economic Analysis):
|
||||
|
||||
|
||||
In 2024, California's growth rate of 6% outpaced the top three economies: U.S. (5.3%), China (2.6%) and Germany (2.9%)...
|
||||
|
||||
|
||||
With an increasing state population and recent record-high tourism spending, California is the nation's top state for new business starts, access to venture capital funding, and manufacturing, high-tech, and agriculture. The state drives national economic growth and also sends over $83 billion more to the federal government than it receives in federal funding. California is the leading agricultural producer in the country and is also the center for manufacturing output in the United States, with over 36,000 manufacturing firms employing over 1.1 million Californians.
|
||||
|
||||
|
||||
The data shows that last year California accounted for 14% of America's GDP, CNN points out, "driven by Silicon Valley and its real estate and finance sectors."<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=California+Becomes+the+World's+Fourth-Largest+Economy%2C+Overtaking+Japan%3A+https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F0625244%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F0625244%2Fcalifornia-becomes-the-worlds-fourth-largest-economy-overtaking-japan%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://news.slashdot.org/story/25/04/26/0625244/california-becomes-the-worlds-fourth-largest-economy-overtaking-japan?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675837&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T18:34:00+00:00</dc:date>
|
||||
<dc:subject>usa</dc:subject>
|
||||
<slash:department>gold-rush</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>127</slash:comments>
|
||||
<slash:hit_parade>127,121,101,87,20,9,4</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://news.slashdot.org/story/25/04/26/0520221/us-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>US Attorney for D.C. Accuses Wikipedia of 'Propaganda', Threatens Nonprofit Status</title>
|
||||
<link>https://news.slashdot.org/story/25/04/26/0520221/us-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>An anonymous reader shared this report from the Washington Post:
|
||||
|
||||
|
||||
The acting U.S. attorney for the District of Columbia sent a letter to the nonprofit that runs Wikipedia, accusing the tax-exempt organization of "allowing foreign actors to manipulate information and spread propaganda to the American public."
|
||||
|
||||
|
||||
In the letter dated April 24, Ed Martin said he sought to determine whether the Wikimedia Foundation's behavior is in violation of its Section 501(c)(3) status. Martin asked the foundation to provide detailed information about its editorial process, its trust and safety measures, and how it protects its information from foreign actors. "Wikipedia is permitting information manipulation on its platform, including the rewriting of key, historical events and biographical information of current and previous American leaders, as well as other matters implicating the national security and the interests of the United States," Martin wrote. "Masking propaganda that influences public opinion under the guise of providing informational material is antithetical to Wikimedia's 'educational' mission."
|
||||
Google prioritizes Wikipedia articles, the letter points out, which "will only amplify propaganda" if the content contained in Wikipedia articles "is biased, unreliable, or sourced by entities who wish to do harm to the United States." And as a U.S.-based non-profit, Wikipedia enjoys tax-exempt status while its board "is composed primarily of foreign nationals," the letter argues, "subverting the interests of American taxpayers."
|
||||
|
||||
While noting Martin's concerns about "allowing foreign actors to manipulate information and spread propaganda," the Washington Post also notes that before being named U.S. attorney, "Martin appeared on Russia-backed media networks more than 150 times, The Washington Post reported last week...."
|
||||
Additional articles about the letter here and here.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=US+Attorney+for+D.C.+Accuses+Wikipedia+of+'Propaganda'%2C+Threatens+Nonprofit+Status%3A+https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F0520221%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F0520221%2Fus-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://news.slashdot.org/story/25/04/26/0520221/us-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675803&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T17:34:00+00:00</dc:date>
|
||||
<dc:subject>usa</dc:subject>
|
||||
<slash:department>burn-it-to-the-wiki</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>155</slash:comments>
|
||||
<slash:hit_parade>155,151,126,105,38,28,23</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://slashdot.org/story/25/04/26/0742205/nyt-asks-should-we-start-taking-the-welfare-of-ai-seriously?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>NYT Asks: Should We Start Taking the Welfare of AI Seriously?</title>
|
||||
<link>https://slashdot.org/story/25/04/26/0742205/nyt-asks-should-we-start-taking-the-welfare-of-ai-seriously?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>A New York Times technology columnist has a question.
|
||||
"Is there any threshold at which an A.I. would start to deserve, if not human-level rights, at least the same moral consideration we give to animals?"
|
||||
|
||||
|
||||
[W]hen I heard that researchers at Anthropic, the AI company that made the Claude chatbot, were starting to study "model welfare" &mdash; the idea that AI models might soon become conscious and deserve some kind of moral status &mdash; the humanist in me thought: Who cares about the chatbots? Aren't we supposed to be worried about AI mistreating us, not us mistreating it...?
|
||||
|
||||
But I was intrigued... There is a small body of academic research on A.I. model welfare, and a modest but growing number of experts in fields like philosophy and neuroscience are taking the prospect of A.I. consciousness more seriously, as A.I. systems grow more intelligent.... Tech companies are starting to talk about it more, too. Google recently posted a job listing for a "post-AGI" research scientist whose areas of focus will include "machine consciousness." And last year, Anthropic hired its first AI welfare researcher, Kyle Fish... [who] believes that in the next few years, as AI models develop more humanlike abilities, AI companies will need to take the possibility of consciousness more seriously....
|
||||
|
||||
|
||||
Fish isn't the only person at Anthropic thinking about AI welfare. There's an active channel on the company's Slack messaging system called #model-welfare, where employees check in on Claude's well-being and share examples of AI systems acting in humanlike ways. Jared Kaplan, Anthropic's chief science officer, said in a separate interview that he thought it was "pretty reasonable" to study AI welfare, given how intelligent the models are getting. But testing AI systems for consciousness is hard, Kaplan warned, because they're such good mimics. If you prompt Claude or ChatGPT to talk about its feelings, it might give you a compelling response. That doesn't mean the chatbot actually has feelings &mdash; only that it knows how to talk about them...
|
||||
|
||||
[Fish] said there were things that AI companies could do to take their models' welfare into account, in case they do become conscious someday. One question Anthropic is exploring, he said, is whether future AI models should be given the ability to stop chatting with an annoying or abusive user if they find the user's requests too distressing.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=NYT+Asks%3A+Should+We+Start+Taking+the+Welfare+of+AI+Seriously%3F%3A+https%3A%2F%2Fslashdot.org%2Fstory%2F25%2F04%2F26%2F0742205%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fslashdot.org%2Fstory%2F25%2F04%2F26%2F0742205%2Fnyt-asks-should-we-start-taking-the-welfare-of-ai-seriously%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://slashdot.org/story/25/04/26/0742205/nyt-asks-should-we-start-taking-the-welfare-of-ai-seriously?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675877&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T16:34:00+00:00</dc:date>
|
||||
<dc:subject>ai</dc:subject>
|
||||
<slash:department>soul-of-a-new-machine</slash:department>
|
||||
<slash:section>slashdot</slash:section>
|
||||
<slash:comments>91</slash:comments>
|
||||
<slash:hit_parade>91,90,86,71,16,7,4</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Cheap 'Transforming' Electric Truck Announced by Jeff Bezos-Backed Startup</title>
|
||||
<link>https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>It's a pickup truck "that can change into whatever you need it to be &mdash; even an SUV," according to the manufacturer's web site.
|
||||
|
||||
Selling in America for just $20,000 (after federal incentives), the new electric truck is "affordable, deeply customizable, and very analog," says TechCrunch. "It has manual windows and it doesn't come with a main infotainment screen. Heck, it isn't even painted..."
|
||||
|
||||
Slate Auto is instead playing up the idea of wrapping its vehicles, something executives said they will sell in kits. Buyers can either have Slate do that work for them, or put the wraps on themselves. This not only adds to the idea of a buyer being able to personalize their vehicle, but it also cuts out a huge cost center for the company. It means Slate won't need a paint shop at its factory, allowing it to spend less to get to market, while also avoiding one of the most heavily regulated parts of vehicle manufacturing.
|
||||
|
||||
Slate is telling customers that they can name the car whatever they want, offering the ability to purchase an embossed wrap for the tailgate. Otherwise, the truck is just referred to as the "Blank Slate...." It's billing the add-ons as "easy DIY" that "non-gearheads" can tackle, and says it will launch a suite of how-to resources under the billing of Slate University... The early library of customizations on Slate's website range from functional to cosmetic. Buyers can add infotainment screens, speakers, roof racks, light covers, and much more.... All that said, Slate's truck comes standard with some federally mandated safety features such as automatic emergency braking, airbags, and a backup camera.
|
||||
"The specs show a maximum range of 150 miles on a single charge, with the option for a longer-range battery pack that could offer up to 240 miles," reports NBC News (adding that the vehicles "aren't expected to be delivered to customers until late 2026, but can be reserved for a refundable $50 fee.")
|
||||
|
||||
Earlier this month, TechCrunch broke the news that Bezos, along with the controlling owner of the Los Angeles Dodgers, Mark Walter; and a third investor, Thomas Tull, had helped Slate raise $111 million for the project. A document filed with the Securities and Exchange Commission listed Melinda Lewison, the head of Bezos' family office, as a Slate Auto director.
|
||||
|
||||
Thanks to Slashdot reader fjo3 for sharing the news.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Cheap+'Transforming'+Electric+Truck+Announced+by+Jeff+Bezos-Backed+Startup%3A+https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F26%2F0425259%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F26%2F0425259%2Fcheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675777&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T15:34:00+00:00</dc:date>
|
||||
<dc:subject>transportation</dc:subject>
|
||||
<slash:department>keep-on-truckin'</slash:department>
|
||||
<slash:section>technology</slash:section>
|
||||
<slash:comments>140</slash:comments>
|
||||
<slash:hit_parade>140,140,135,123,18,8,2</slash:hit_parade>
|
||||
</item>
|
||||
<textinput rdf:about="https://slashdot.org/search.pl">
|
||||
<title>Search Slashdot</title>
|
||||
<description>Search Slashdot stories</description>
|
||||
<name>query</name>
|
||||
<link>https://slashdot.org/search.pl</link>
|
||||
</textinput>
|
||||
</rdf:RDF>
|
||||
117
src/cmd/server/handler/ui.go
Normal file
117
src/cmd/server/handler/ui.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"show-rss/src/feeds"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"embed"
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed public/index.tmpl
|
||||
var embeddedIndexTMPL string
|
||||
|
||||
//go:embed public/*
|
||||
var embeddedDir embed.FS
|
||||
|
||||
var dir = func() string {
|
||||
if v := os.Getenv("UI_D"); v != "" {
|
||||
return v
|
||||
}
|
||||
return "./src/cmd/server/handler/public"
|
||||
}()
|
||||
|
||||
func (h Handler) ui(w http.ResponseWriter, r *http.Request) error {
|
||||
if path.Base(r.URL.Path) == "ui" || r.URL.Path == "/" {
|
||||
return h.uiIndex(w, r)
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "max-age=2592000")
|
||||
fs := http.FileServer(http.FS(embeddedDir))
|
||||
r.URL.Path = fmt.Sprintf("/public/%s", strings.TrimPrefix(r.URL.Path, "/experimental/ui"))
|
||||
fs.ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h Handler) uiIndex(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := r.Context()
|
||||
|
||||
var editing struct {
|
||||
feeds.Entry `json:",inline"`
|
||||
feeds.Version `json:",inline"`
|
||||
}
|
||||
editing.Version.Cron = fmt.Sprintf("%d */12 * * *", rand.Int()%60)
|
||||
editing.Version.URL = "nyaa://?q=SOME_SHOW dual web-dl"
|
||||
editing.Version.Pattern = ".*"
|
||||
editing.Version.WebhookBody = "{{ .Item.Link }}"
|
||||
editing.Version.WebhookMethod = "POST"
|
||||
editing.Version.WebhookURL = "vpntor:///data/completed-rss/TITLE"
|
||||
all := []feeds.Feed{}
|
||||
if err := feeds.ForEach(ctx, func(f feeds.Feed) error {
|
||||
if deleted := f.Version.URL == ""; !deleted {
|
||||
all = append(all, f)
|
||||
}
|
||||
if f.Entry.ID == r.URL.Query().Get("edit") {
|
||||
editing.Entry = f.Entry
|
||||
editing.Version = f.Version
|
||||
}
|
||||
return ctx.Err()
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, _ := os.ReadFile(path.Join(dir, "index.tmpl"))
|
||||
if len(b) == 0 {
|
||||
b = []byte(embeddedIndexTMPL)
|
||||
}
|
||||
|
||||
tmpl := template.New(r.URL.Path).Funcs(template.FuncMap{
|
||||
"feeds": func() []feeds.Feed {
|
||||
return all
|
||||
},
|
||||
"feedsVersionFields": func() []string {
|
||||
b, _ := json.Marshal(feeds.Version{})
|
||||
var m map[string]any
|
||||
json.Unmarshal(b, &m)
|
||||
ks := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
slices.Sort(ks)
|
||||
return ks
|
||||
},
|
||||
"in": func(k string, v ...string) bool {
|
||||
return slices.Contains(v, k)
|
||||
},
|
||||
"ago": func(t time.Time) time.Duration {
|
||||
return time.Since(t)
|
||||
},
|
||||
})
|
||||
|
||||
tmpl, err := tmpl.Parse(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := map[string]any{}
|
||||
|
||||
{
|
||||
b, _ := json.Marshal(editing)
|
||||
var m map[string]any
|
||||
json.Unmarshal(b, &m)
|
||||
args["editing"] = m
|
||||
|
||||
args["editing_url"], _ = editing.FetchURL()
|
||||
}
|
||||
|
||||
return tmpl.Execute(w, args)
|
||||
}
|
||||
65
src/cmd/server/handler/vpntor.go
Normal file
65
src/cmd/server/handler/vpntor.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (h Handler) vpntor(ctx context.Context, r io.Reader) error {
|
||||
var form struct {
|
||||
Magnet string
|
||||
Dir string
|
||||
URL string
|
||||
}
|
||||
if err := json.NewDecoder(r).Decode(&form); err != nil {
|
||||
return err
|
||||
}
|
||||
if form.Magnet == "" || form.Dir == "" || form.URL == "" {
|
||||
return fmt.Errorf("did not specify all of .Magnet, .Dir, .URL")
|
||||
}
|
||||
|
||||
c := http.Client{
|
||||
Timeout: time.Minute,
|
||||
Transport: &http.Transport{DisableKeepAlives: true},
|
||||
}
|
||||
sresp, err := c.Get(form.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sresp.Body.Close()
|
||||
defer io.Copy(io.Discard, sresp.Body)
|
||||
|
||||
b, _ := json.Marshal(map[string]any{
|
||||
"method": "torrent-add",
|
||||
"arguments": map[string]string{
|
||||
"filename": form.Magnet,
|
||||
"download-dir": form.Dir,
|
||||
},
|
||||
})
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, form.URL, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Add("X-Transmission-Session-Id", sresp.Header.Get("X-Transmission-Session-Id"))
|
||||
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer io.Copy(io.Discard, resp.Body)
|
||||
|
||||
if b, _ := ioutil.ReadAll(resp.Body); resp.StatusCode > 220 || !bytes.Contains(b, []byte(`success`)) {
|
||||
return fmt.Errorf("(%d) %s", resp.StatusCode, b)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
66
src/cmd/server/handler/vpntor_test.go
Normal file
66
src/cmd/server/handler/vpntor_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"show-rss/src/cmd/server/handler"
|
||||
"show-rss/src/db"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVpntor(t *testing.T) {
|
||||
h := handler.New(db.Test(t, context.Background()))
|
||||
|
||||
t.Run("no body", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/v1/vpntor", nil)
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("%+v", w)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bad body", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodPost, "/v1/vpntor", strings.NewReader(`{}`))
|
||||
h.ServeHTTP(w, r)
|
||||
t.Logf("%s", w.Body.Bytes())
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("%+v", w.Code)
|
||||
}
|
||||
if !strings.Contains(string(w.Body.Bytes()), `.Magnet, .Dir, .URL`) {
|
||||
t.Errorf("%+v", string(w.Body.Bytes()))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("doit", func(t *testing.T) {
|
||||
calls := 0
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
t.Logf("%s | %s", r.Header.Get("X-Transmission-Session-Id"), b)
|
||||
calls += 1
|
||||
w.Header().Set("X-Transmission-Session-Id", "session")
|
||||
w.Write([]byte(`success`))
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodPost, "/v1/vpntor", strings.NewReader(fmt.Sprintf(`{
|
||||
"Magnet": %q,
|
||||
"Dir": %q,
|
||||
"URL": %q
|
||||
}`, "magnet", "dir", s.URL)))
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("%+v", w.Code)
|
||||
}
|
||||
|
||||
if calls != 2 {
|
||||
t.Errorf("expected 2 but got %d calls", calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,9 +2,41 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"show-rss/src/cmd/server/handler"
|
||||
"show-rss/src/server"
|
||||
)
|
||||
|
||||
func Main(ctx context.Context) error {
|
||||
return io.EOF
|
||||
return Run(ctx, fmt.Sprintf(":%d", server.Extract(ctx)))
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, listen string) error {
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
defer can()
|
||||
|
||||
s := http.Server{
|
||||
Addr: listen,
|
||||
Handler: handler.New(ctx),
|
||||
BaseContext: func(net.Listener) context.Context { return ctx },
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
select {
|
||||
case errs <- s.ListenAndServe():
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case err := <-errs:
|
||||
return err
|
||||
}
|
||||
return s.Close()
|
||||
}
|
||||
|
||||
50
src/cmd/server/main_test.go
Normal file
50
src/cmd/server/main_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"show-rss/src/cmd/server"
|
||||
"show-rss/src/db"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestServerStarts(t *testing.T) {
|
||||
With(t, func(url string) {
|
||||
})
|
||||
}
|
||||
|
||||
func With(t *testing.T, cb func(url string)) {
|
||||
ctx, can := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer can()
|
||||
|
||||
listen := func() string {
|
||||
s := httptest.NewServer(http.HandlerFunc(http.NotFound))
|
||||
s.Close()
|
||||
u, _ := url.Parse(s.URL)
|
||||
return u.Host
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := server.Run(db.Test(t, ctx), listen); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
url := "http://" + listen
|
||||
for {
|
||||
if resp, err := http.Get(url); err == nil {
|
||||
resp.Body.Close()
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
case <-ctx.Done():
|
||||
t.Fatal(ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
cb(url)
|
||||
}
|
||||
31
src/cron/cron.go
Normal file
31
src/cron/cron.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Cron(ctx context.Context, next func(context.Context) (time.Time, error), do func(ctx context.Context) error) error {
|
||||
n, err := next(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := time.NewTicker(3 * time.Minute)
|
||||
defer c.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-c.C:
|
||||
n, err = next(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-time.After(time.Until(n)):
|
||||
if err := do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"database/sql"
|
||||
@@ -14,7 +17,28 @@ import (
|
||||
|
||||
const ctxKey = "__db"
|
||||
|
||||
func Test(t *testing.T, ctx context.Context) context.Context {
|
||||
p := path.Join(t.TempDir(), strings.ReplaceAll(t.Name()+".db", "/", "_"))
|
||||
t.Logf("test db @ %s", p)
|
||||
ctx, err := Inject(ctx, p)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to inject db %s: %v", p, err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
db, _, err := extract(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.Close()
|
||||
})
|
||||
return ctx
|
||||
}
|
||||
|
||||
func Inject(ctx context.Context, conn string) (context.Context, error) {
|
||||
if _, _, err := extract(ctx); err == nil {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
connctx, can := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer can()
|
||||
|
||||
@@ -45,13 +69,13 @@ func Inject(ctx context.Context, conn string) (context.Context, error) {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
return context.WithValue(ctx, ctxKey, db), ctx.Err()
|
||||
return context.WithValue(context.WithValue(ctx, ctxKey+"_lock", newSemaphore()), ctxKey, db), ctx.Err()
|
||||
}
|
||||
|
||||
func extract(ctx context.Context) (*sql.DB, error) {
|
||||
func extract(ctx context.Context) (*sql.DB, semaphore, error) {
|
||||
db := ctx.Value(ctxKey)
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("db not injected")
|
||||
return nil, nil, fmt.Errorf("db not injected")
|
||||
}
|
||||
return db.(*sql.DB), nil
|
||||
return db.(*sql.DB), ctx.Value(ctxKey + "_lock").(semaphore), nil
|
||||
}
|
||||
|
||||
54
src/db/db.go
54
src/db/db.go
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func QueryOne[T any](ctx context.Context, q string, args ...any) (T, error) {
|
||||
@@ -33,12 +34,33 @@ func Query[T any](ctx context.Context, q string, args ...any) ([]T, error) {
|
||||
}
|
||||
scanners := func(columns []string) ([]any, error) {
|
||||
s := make([]any, len(columns))
|
||||
for i, k := range columns {
|
||||
v, ok := m[k]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot scan column %s to %T (%+v)", k, a, m)
|
||||
i := 0
|
||||
for i < len(columns) {
|
||||
k := columns[i]
|
||||
if strings.Contains(k, ".") {
|
||||
columns := strings.SplitN(k, ".", 2)
|
||||
m2, ok := m[columns[0]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no column %s in %T (%+v)", columns[0], a, m)
|
||||
}
|
||||
m3, ok := m2.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot scan subfield %s of %s of %T (%+v)", columns[1], columns[0], a, m)
|
||||
}
|
||||
v, ok := m3[columns[1]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no subfield %s of %s of %T (%+v)", columns[1], columns[0], a, m)
|
||||
}
|
||||
s[i] = &v
|
||||
i += 1
|
||||
} else {
|
||||
v, ok := m[k]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no column %s in %T (%+v)", k, a, m)
|
||||
}
|
||||
s[i] = &v
|
||||
i += 1
|
||||
}
|
||||
s[i] = &v
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
@@ -67,7 +89,21 @@ func Query[T any](ctx context.Context, q string, args ...any) ([]T, error) {
|
||||
|
||||
m := map[string]any{}
|
||||
for i, column := range columns {
|
||||
m[column] = scanners[i]
|
||||
if !strings.Contains(column, ".") {
|
||||
m[column] = scanners[i]
|
||||
} else {
|
||||
columns := strings.SplitN(column, ".", 2)
|
||||
m2, ok := m[columns[0]]
|
||||
if !ok {
|
||||
m2 = map[string]any{}
|
||||
}
|
||||
m3, ok := m2.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s is not a submap", columns[0])
|
||||
}
|
||||
m3[columns[1]] = scanners[i]
|
||||
m[columns[0]] = m3
|
||||
}
|
||||
}
|
||||
|
||||
var a T
|
||||
@@ -93,9 +129,11 @@ func Exec(ctx context.Context, q string, args ...any) error {
|
||||
}
|
||||
|
||||
func with(ctx context.Context, foo func(*sql.DB) error) error {
|
||||
db, err := extract(ctx)
|
||||
db, sem, err := extract(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return foo(db)
|
||||
return sem.With(ctx, func() error {
|
||||
return foo(db)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,4 +46,15 @@ func TestDB(t *testing.T) {
|
||||
} else if gots[1].K != "b" {
|
||||
t.Errorf("expected [1]='b' but got %q", gots[1].K)
|
||||
}
|
||||
|
||||
type NestedResult struct {
|
||||
Nest struct {
|
||||
K string `json:"k"`
|
||||
}
|
||||
}
|
||||
if got, err := db.QueryOne[NestedResult](ctx, `SELECT k AS "Nest.k" FROM test WHERE k='a'`); err != nil {
|
||||
t.Errorf("failed nested query one: %v", err)
|
||||
} else if got.Nest.K != "a" {
|
||||
t.Errorf("bad nested query one: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
38
src/db/schema.go
Normal file
38
src/db/schema.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func InitializeSchema(ctx context.Context, k string, mods []string) error {
|
||||
if err := Exec(ctx, fmt.Sprintf(`CREATE TABLE IF NOT EXISTS "database_version.%s" (v NUMBER, t TIMESTAMP)`, k)); err != nil {
|
||||
return fmt.Errorf(`failed to create "database_version.%s" table: %w`, k, err)
|
||||
}
|
||||
|
||||
type DatabaseVersion struct {
|
||||
V int `json:"v"`
|
||||
T time.Time `json:"t"`
|
||||
}
|
||||
vs, err := Query[DatabaseVersion](ctx, fmt.Sprintf(`SELECT v, t FROM "database_version.%s" ORDER BY v DESC LIMIT 1`, k))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v DatabaseVersion
|
||||
if len(vs) > 0 {
|
||||
v = vs[0]
|
||||
}
|
||||
|
||||
mods = append([]string{""}, mods...)
|
||||
for i := v.V + 1; i < len(mods); i++ {
|
||||
q := mods[i]
|
||||
if err := Exec(ctx, q, i, time.Now()); err != nil {
|
||||
return fmt.Errorf("[%s][%d] failed mod %s: %w", k, i, mods[i], err)
|
||||
} else if err := Exec(ctx, fmt.Sprintf(`INSERT INTO "database_version.%s" (v, t) VALUES (?, ?)`, k), i, time.Now()); err != nil {
|
||||
return fmt.Errorf("[%s][%d] failed ack mod %s: %w", k, i, mods[i], err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
21
src/db/sem.go
Normal file
21
src/db/sem.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package db
|
||||
|
||||
import "context"
|
||||
|
||||
type semaphore chan struct{}
|
||||
|
||||
func newSemaphore() semaphore {
|
||||
return make(semaphore, 1)
|
||||
}
|
||||
|
||||
func (semaphore semaphore) With(ctx context.Context, cb func() error) error {
|
||||
select {
|
||||
case semaphore <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
return cb()
|
||||
}
|
||||
296
src/feeds/db.go
Normal file
296
src/feeds/db.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package feeds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"show-rss/src/db"
|
||||
"show-rss/src/server"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type (
|
||||
Feed struct {
|
||||
Entry Entry
|
||||
Version Version
|
||||
Execution Execution
|
||||
}
|
||||
|
||||
Entry struct {
|
||||
ID string
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
Deleted time.Time
|
||||
}
|
||||
|
||||
Version struct {
|
||||
Created time.Time
|
||||
URL string
|
||||
Cron string
|
||||
Pattern string
|
||||
WebhookMethod string
|
||||
WebhookURL string
|
||||
WebhookBody string
|
||||
}
|
||||
|
||||
Execution struct {
|
||||
Executed time.Time
|
||||
Version time.Time
|
||||
}
|
||||
)
|
||||
|
||||
func Next(ctx context.Context) (time.Time, error) {
|
||||
result := time.Now().Add(3 * time.Minute)
|
||||
err := ForEach(ctx, func(f Feed) error {
|
||||
next, err := f.Next()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if next.After(result) {
|
||||
return nil
|
||||
}
|
||||
|
||||
result = next
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func ForEach(ctx context.Context, cb func(Feed) error) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type id struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
ids, err := db.Query[id](ctx, `SELECT id FROM "feed.entries" WHERE deleted_at IS NULL`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []string
|
||||
for _, id := range ids {
|
||||
feed, err := Get(ctx, id.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if err := cb(feed); err != nil {
|
||||
errs = append(errs, fmt.Sprintf(`failed to fetch %s: %v`, id.ID, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("failed some callbacks: %+v", errs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Delete(ctx context.Context, id string) error {
|
||||
return Update(ctx, id, "", "", "", "", "", "")
|
||||
}
|
||||
|
||||
func Get(ctx context.Context, id string) (Feed, error) {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return Feed{}, err
|
||||
}
|
||||
|
||||
return db.QueryOne[Feed](ctx, `
|
||||
WITH
|
||||
entry AS (
|
||||
SELECT
|
||||
id AS ID,
|
||||
created_at AS Created,
|
||||
updated_at AS Updated,
|
||||
deleted_at AS Deleted
|
||||
FROM "feed.entries"
|
||||
WHERE id = ?
|
||||
),
|
||||
execution AS (
|
||||
SELECT
|
||||
executed_at AS Executed,
|
||||
versions_created_at AS Version
|
||||
FROM "feed.executions"
|
||||
WHERE entries_id = ?
|
||||
ORDER BY executed DESC
|
||||
LIMIT 1
|
||||
)
|
||||
SELECT
|
||||
entry.ID AS "Entry.ID",
|
||||
entry.Created AS "Entry.Created",
|
||||
entry.Updated AS "Entry.Updated",
|
||||
entry.Deleted AS "Entry.Deleted",
|
||||
versions.created_at AS "Version.Created",
|
||||
versions.url AS "Version.URL",
|
||||
versions.cron AS "Version.Cron",
|
||||
versions.pattern AS "Version.Pattern",
|
||||
versions.webhook_method AS "Version.WebhookMethod",
|
||||
versions.webhook_url AS "Version.WebhookURL",
|
||||
versions.webhook_body AS "Version.WebhookBody",
|
||||
(
|
||||
SELECT executed_at
|
||||
FROM "feed.executions"
|
||||
WHERE entries_id = entry.ID
|
||||
ORDER BY executed_at DESC
|
||||
LIMIT 1
|
||||
) AS "Execution.Executed",
|
||||
(
|
||||
SELECT versions_created_at
|
||||
FROM "feed.executions"
|
||||
WHERE entries_id = entry.ID
|
||||
ORDER BY executed_at DESC
|
||||
LIMIT 1
|
||||
) AS "Execution.Version"
|
||||
FROM entry
|
||||
JOIN "feed.versions" versions ON
|
||||
versions.created_at=entry.Updated
|
||||
WHERE versions.entries_id=entry.ID
|
||||
`, id, id)
|
||||
}
|
||||
|
||||
func (feed Feed) Executed(ctx context.Context) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id := feed.Entry.ID
|
||||
version := feed.Version.Created
|
||||
|
||||
now := time.Now()
|
||||
return db.Exec(ctx, `
|
||||
INSERT INTO "feed.executions" (
|
||||
entries_id,
|
||||
versions_created_at,
|
||||
executed_at
|
||||
) VALUES (?, ?, ?);
|
||||
`,
|
||||
id, version, now,
|
||||
)
|
||||
}
|
||||
|
||||
func Insert(ctx context.Context, url, cron, pattern, webhookMethod, webhookURL, webhookBody string) (string, error) {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
id := uuid.New().String()
|
||||
return id, db.Exec(ctx, `
|
||||
BEGIN;
|
||||
INSERT INTO "feed.entries" (
|
||||
id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES ($1, $2, $3);
|
||||
INSERT INTO "feed.versions" (
|
||||
entries_id,
|
||||
created_at,
|
||||
url,
|
||||
cron,
|
||||
pattern,
|
||||
webhook_method,
|
||||
webhook_url,
|
||||
webhook_body
|
||||
) VALUES ($4, $5, $6, $7, $8, $9, $10, $11);
|
||||
COMMIT;
|
||||
`,
|
||||
id, now, now,
|
||||
id, now, url, cron, pattern, webhookMethod, webhookURL, webhookBody,
|
||||
)
|
||||
}
|
||||
|
||||
func Update(ctx context.Context, id string, url, cron, pattern, webhookMethod, webhookURL, webhookBody string) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := Get(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return db.Exec(ctx, `
|
||||
BEGIN;
|
||||
UPDATE "feed.entries" SET updated_at=$1 WHERE id=$2;
|
||||
INSERT INTO "feed.versions" (
|
||||
entries_id,
|
||||
created_at,
|
||||
url,
|
||||
cron,
|
||||
pattern,
|
||||
webhook_method,
|
||||
webhook_url,
|
||||
webhook_body
|
||||
) VALUES ($3, $4, $5, $6, $7, $8, $9, $10);
|
||||
COMMIT;
|
||||
`,
|
||||
now, id,
|
||||
id, now, url, cron, pattern, webhookMethod, webhookURL, webhookBody,
|
||||
)
|
||||
}
|
||||
|
||||
func getEntry(ctx context.Context, id string) (Entry, error) {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return Entry{}, err
|
||||
}
|
||||
|
||||
return db.QueryOne[Entry](ctx, `
|
||||
SELECT
|
||||
id AS ID,
|
||||
created_at AS Created,
|
||||
updated_at AS Updated,
|
||||
deleted_at AS Deleted
|
||||
FROM
|
||||
"feed.entries"
|
||||
WHERE
|
||||
id = ?
|
||||
`, id)
|
||||
}
|
||||
|
||||
func initDB(ctx context.Context) error {
|
||||
return db.InitializeSchema(ctx, "feeds", []string{
|
||||
`CREATE TABLE "feed.entries" (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
deleted_at TIMESTAMP
|
||||
)`,
|
||||
|
||||
`CREATE TABLE "feed.versions" (
|
||||
entries_id TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (entries_id, created_at),
|
||||
FOREIGN KEY (entries_id) REFERENCES "feed.entries" (id)
|
||||
)`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN url TEXT NOT NULL`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN cron TEXT NOT NULL DEFAULT '0 0 * * *'`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN pattern TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN webhook_method TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN webhook_url TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN webhook_body TEXT NOT NULL DEFAULT ''`,
|
||||
|
||||
`CREATE TABLE "feed.executions" (
|
||||
entries_id TEXT,
|
||||
versions_created_at TIMESTAMP NOT NULL,
|
||||
executed_at TIMESTAMP,
|
||||
FOREIGN KEY (entries_id, versions_created_at) REFERENCES "feed.versions" (entries_id, created_at)
|
||||
)`,
|
||||
})
|
||||
}
|
||||
|
||||
func (feed Feed) Webhook(ctx context.Context) (string, string, string) {
|
||||
u, _ := url.Parse(feed.Version.WebhookURL)
|
||||
switch u.Scheme {
|
||||
case "vpntor":
|
||||
return "POST", fmt.Sprintf("http://localhost:%d/v1/vpntor", server.Extract(ctx)), fmt.Sprintf(`{
|
||||
"Magnet": "{{ .Item.Link }}",
|
||||
"Dir": %q,
|
||||
"URL": "https://vpntor.int.bel.blue/transmission/rpc"
|
||||
}`, u.Path)
|
||||
default:
|
||||
return feed.Version.WebhookMethod, feed.Version.WebhookURL, feed.Version.WebhookBody
|
||||
}
|
||||
}
|
||||
142
src/feeds/db_test.go
Normal file
142
src/feeds/db_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package feeds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"show-rss/src/db"
|
||||
"show-rss/src/feeds"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFeeds(t *testing.T) {
|
||||
ctx, can := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer can()
|
||||
|
||||
t.Run("crud", func(t *testing.T) {
|
||||
ctx := db.Test(t, ctx)
|
||||
|
||||
id, err := feeds.Insert(ctx, "url", "cron", "pattern", "wmethod", "wurl", "wbody")
|
||||
if err != nil {
|
||||
t.Fatal("cannot insert:", err)
|
||||
}
|
||||
|
||||
got, err := feeds.Get(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatal("cannot get:", err)
|
||||
}
|
||||
t.Logf("%+v", got)
|
||||
|
||||
if got.Entry.ID == "" {
|
||||
t.Error("no entry.id")
|
||||
}
|
||||
if got.Entry.Created.IsZero() {
|
||||
t.Error("no entry.created")
|
||||
}
|
||||
if got.Entry.Updated.IsZero() {
|
||||
t.Error("no entry.updated")
|
||||
}
|
||||
if !got.Entry.Deleted.IsZero() {
|
||||
t.Error("entry.deleted")
|
||||
}
|
||||
|
||||
if got.Version.Created.IsZero() {
|
||||
t.Error("no version.created")
|
||||
}
|
||||
if got.Version.URL != "url" {
|
||||
t.Error("no version.url")
|
||||
}
|
||||
if got.Version.Cron != "cron" {
|
||||
t.Error("no version.cron")
|
||||
}
|
||||
if got.Version.Pattern != "pattern" {
|
||||
t.Error("bad version.pattern")
|
||||
}
|
||||
if got.Version.WebhookMethod != "wmethod" {
|
||||
t.Error("bad version.webhookMethod")
|
||||
}
|
||||
if got.Version.WebhookURL != "wurl" {
|
||||
t.Error("bad version.webhookURL")
|
||||
}
|
||||
if got.Version.WebhookBody != "wbody" {
|
||||
t.Error("bad version.webhookBody")
|
||||
}
|
||||
|
||||
if !got.Execution.Executed.IsZero() {
|
||||
t.Error("execution.executed")
|
||||
}
|
||||
if !got.Execution.Version.IsZero() {
|
||||
t.Error("execution.version")
|
||||
}
|
||||
|
||||
if err := got.Executed(ctx); err != nil {
|
||||
t.Fatal("cannot executed:", err)
|
||||
}
|
||||
|
||||
got2, err := feeds.Get(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatal("cannot get w executed:", err)
|
||||
}
|
||||
t.Logf("%+v", got2)
|
||||
|
||||
if got2.Execution.Executed.IsZero() {
|
||||
t.Error("no execution.executed")
|
||||
}
|
||||
if got2.Execution.Version != got.Version.Created {
|
||||
t.Errorf("bad execution.version: expected %v but got %v (difference of %v)", got.Version.Created, got2.Execution.Version, got2.Execution.Version.Sub(got.Execution.Version))
|
||||
}
|
||||
|
||||
got2.Execution = got.Execution
|
||||
if got != got2 {
|
||||
t.Errorf("changes after execution: was \n\t%+v but now \n\t%+v", got, got2)
|
||||
}
|
||||
|
||||
if err := got.Executed(ctx); err != nil {
|
||||
t.Fatal("cannot executed again:", err)
|
||||
}
|
||||
got3, err := feeds.Get(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatal("cannot get w executed again:", err)
|
||||
} else if got2.Execution == got3.Execution {
|
||||
t.Errorf("getting after second execution returned first execution")
|
||||
}
|
||||
|
||||
n := 0
|
||||
if err := feeds.ForEach(ctx, func(feed feeds.Feed) error {
|
||||
n += 1
|
||||
if feed != got3 {
|
||||
t.Errorf("for each yielded difference than last get")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
} else if n == 0 {
|
||||
t.Errorf("for each didnt hit known get")
|
||||
}
|
||||
|
||||
if err := feeds.Update(ctx, id, "url2", "cron2", "pattern2", "wmethod2", "wurl2", "wbody2"); err != nil {
|
||||
t.Fatal("cannot update:", err)
|
||||
}
|
||||
got, err = feeds.Get(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatal("cannot get updated:", err)
|
||||
}
|
||||
if v := got.Version.URL; v != "url2" {
|
||||
t.Error(v)
|
||||
}
|
||||
if v := got.Version.Cron; v != "cron2" {
|
||||
t.Error(v)
|
||||
}
|
||||
if v := got.Version.Pattern; v != "pattern2" {
|
||||
t.Error(v)
|
||||
}
|
||||
if v := got.Version.WebhookMethod; v != "wmethod2" {
|
||||
t.Error(v)
|
||||
}
|
||||
if v := got.Version.WebhookURL; v != "wurl2" {
|
||||
t.Error(v)
|
||||
}
|
||||
if v := got.Version.WebhookBody; v != "wbody2" {
|
||||
t.Error(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
181
src/feeds/http.go
Normal file
181
src/feeds/http.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package feeds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jaytaylor/html2text"
|
||||
"github.com/mmcdole/gofeed"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
ProxyU = func() *url.URL {
|
||||
u, err := url.Parse("socks5://wghttp.inhome.blapointe.com:63114")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u
|
||||
}()
|
||||
)
|
||||
|
||||
func (feed Feed) ShouldExecute() (bool, error) {
|
||||
if !feed.Entry.Deleted.IsZero() || feed.Version.URL == "" {
|
||||
return false, nil
|
||||
}
|
||||
next, err := feed.Next()
|
||||
return time.Now().After(next), err
|
||||
}
|
||||
|
||||
func (feed Feed) Next() (time.Time, error) {
|
||||
schedule, err := cron.NewParser(
|
||||
cron.SecondOptional |
|
||||
cron.Minute |
|
||||
cron.Hour |
|
||||
cron.Dom |
|
||||
cron.Month |
|
||||
cron.Dow |
|
||||
cron.Descriptor,
|
||||
).Parse(feed.Version.Cron)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("illegal cron %q", feed.Version.Cron)
|
||||
}
|
||||
return schedule.Next(feed.Execution.Executed), nil
|
||||
}
|
||||
|
||||
func (feed Feed) Fetch(ctx context.Context) (Items, error) {
|
||||
u, err := feed.FetchURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := proxyFetch(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gfeed, err := gofeed.NewParser().ParseString(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Sort(gfeed)
|
||||
|
||||
matcher := regexp.MustCompile(feed.Version.Pattern)
|
||||
|
||||
result := make(Items, 0, len(gfeed.Items))
|
||||
for _, gitem := range gfeed.Items {
|
||||
if gitem.Author == nil {
|
||||
gitem.Author = &gofeed.Person{}
|
||||
}
|
||||
if matches := slices.DeleteFunc(append([]string{
|
||||
gitem.Title,
|
||||
gitem.Description,
|
||||
gitem.Content,
|
||||
gitem.Link,
|
||||
gitem.Author.Name,
|
||||
gitem.Author.Email,
|
||||
}, gitem.Links...), func(s string) bool {
|
||||
return !matcher.MatchString(s)
|
||||
}); len(matches) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
preview, _ := html2text.FromString(gitem.Description)
|
||||
body, _ := html2text.FromString(gitem.Content)
|
||||
if body == "" {
|
||||
body = preview
|
||||
if len(preview) > 53 {
|
||||
preview = preview[:50] + "..."
|
||||
}
|
||||
}
|
||||
|
||||
links := slices.DeleteFunc(append([]string{gitem.Link}, gitem.Links...), func(s string) bool {
|
||||
return strings.TrimSpace(s) == ""
|
||||
})
|
||||
slices.Sort(links)
|
||||
links = slices.Compact(links)
|
||||
|
||||
var link string
|
||||
if len(links) > 0 {
|
||||
link = links[0]
|
||||
}
|
||||
|
||||
result = append(result, Item{
|
||||
Title: gitem.Title,
|
||||
Link: link,
|
||||
Links: links,
|
||||
Preview: preview,
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func proxyFetch(ctx context.Context, u string) (string, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
c := http.Client{Timeout: time.Minute}
|
||||
if ProxyU != nil {
|
||||
c.Transport = &http.Transport{Proxy: http.ProxyURL(ProxyU)}
|
||||
}
|
||||
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer io.Copy(io.Discard, resp.Body)
|
||||
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed fetch %s %s: (%d) %s", req.Method, req.URL.String(), resp.StatusCode, b)
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (feed Feed) FetchURL() (*url.URL, error) {
|
||||
return feed.Version.FetchURL()
|
||||
}
|
||||
|
||||
func (version Version) FetchURL() (*url.URL, error) {
|
||||
u, err := url.Parse(version.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "nyaa": // `nyaa://?q=A B` to `https://nyaa.si/?page=rss&q=A%20B&c=0_0&f=0`
|
||||
q := u.Query()
|
||||
if q.Get("q") == "" {
|
||||
return nil, fmt.Errorf("invalid nyaa:// (%s): no ?q", version.URL)
|
||||
}
|
||||
|
||||
q.Set("page", "rss")
|
||||
q.Set("c", "0_0")
|
||||
q.Set("f", "0")
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
u.Scheme = "https"
|
||||
u.Host = "nyaa.si"
|
||||
u.Path = "/"
|
||||
case "http", "https":
|
||||
default:
|
||||
return nil, fmt.Errorf("not impl mapping %s:// to url", u.Scheme)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
52
src/feeds/http_integration_test.go
Normal file
52
src/feeds/http_integration_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
//go:build integration
|
||||
|
||||
package feeds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"show-rss/src/feeds"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIntegrationFeedFetchProxy(t *testing.T) {
|
||||
os.Setenv("PROXY", "socks5://wghttp.inhome.blapointe.com:63114")
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("PROXY")
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
created := time.Now().Add(-4 * time.Second)
|
||||
feed := feeds.Feed{
|
||||
Entry: feeds.Entry{
|
||||
ID: "id",
|
||||
Created: created,
|
||||
Updated: created,
|
||||
Deleted: time.Time{},
|
||||
},
|
||||
Version: feeds.Version{
|
||||
Created: created,
|
||||
URL: "https://nyaa.si/?f=0&c=0_0&q=Zenshuu.+WEB+ToonsHub+AMZN&page=rss",
|
||||
Cron: "* * * * *",
|
||||
Pattern: `\.torrent$`,
|
||||
Tag: `outdir:/data/completed-rss/Zenshuu`,
|
||||
},
|
||||
Execution: feeds.Execution{
|
||||
Executed: created.Add(-2 * time.Second),
|
||||
Version: created,
|
||||
},
|
||||
}
|
||||
|
||||
items, err := feed.Fetch(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed fetch: %v", err)
|
||||
}
|
||||
for i, item := range items {
|
||||
t.Logf("[%d] %+v", i, item)
|
||||
if item.Tag != "outdir:/data/completed-rss/Zenshuu" {
|
||||
t.Errorf("[%d] wrong tag: %s", i, item.Tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/feeds/http_test.go
Normal file
98
src/feeds/http_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package feeds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"show-rss/src/feeds"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFeedFetch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
proxyU := feeds.ProxyU
|
||||
feeds.ProxyU = nil
|
||||
t.Cleanup(func() {
|
||||
feeds.ProxyU = proxyU
|
||||
})
|
||||
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("%s", r.URL.String())
|
||||
|
||||
b, _ := os.ReadFile("testdata/slashdot.rss")
|
||||
w.Write(b)
|
||||
}))
|
||||
t.Cleanup(s.Close)
|
||||
|
||||
created := time.Now().Add(-4 * time.Second)
|
||||
feed := feeds.Feed{
|
||||
Entry: feeds.Entry{
|
||||
ID: "id",
|
||||
Created: created,
|
||||
Updated: created,
|
||||
Deleted: time.Time{},
|
||||
},
|
||||
Version: feeds.Version{
|
||||
Created: created,
|
||||
URL: s.URL,
|
||||
Cron: "* * * * *",
|
||||
Pattern: ".*",
|
||||
},
|
||||
Execution: feeds.Execution{
|
||||
Executed: created.Add(-2 * time.Second),
|
||||
Version: created,
|
||||
},
|
||||
}
|
||||
|
||||
items, err := feed.Fetch(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed fetch: %v", err)
|
||||
}
|
||||
for i, item := range items {
|
||||
t.Logf("[%d] %+v", i, item)
|
||||
}
|
||||
|
||||
if len(items) != 15 {
|
||||
t.Fatalf("expected 15 items but got %d", len(items))
|
||||
}
|
||||
|
||||
if items[0].Body == "" {
|
||||
t.Errorf("no body")
|
||||
}
|
||||
items[0].Body = ""
|
||||
|
||||
expect := feeds.Item{
|
||||
Title: `Cheap 'Transforming' Electric Truck Announced by Jeff Bezos-Backed Startup`,
|
||||
Link: `https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0mainlinkanon&utm_medium=feed`,
|
||||
Links: []string{`https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0mainlinkanon&utm_medium=feed`},
|
||||
Preview: `It's a pickup truck "that can change into whatever...`,
|
||||
}
|
||||
if fmt.Sprintf("%+v", items[0]) != fmt.Sprintf("%+v", expect) {
|
||||
t.Errorf("expected\n\t%+v but got \n\t%+v", expect, items[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedFetchURL(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"http://host/path?k=v": "http://host/path?k=v",
|
||||
"https://host/path?k=v": "https://host/path?k=v",
|
||||
"nyaa://?q=a b&u=c d": "https://nyaa.si/?c=0_0&f=0&page=rss&q=a+b&u=c+d",
|
||||
}
|
||||
|
||||
for given, want := range cases {
|
||||
given := given
|
||||
want := want
|
||||
t.Run(given, func(t *testing.T) {
|
||||
f := feeds.Feed{}
|
||||
f.Version.URL = given
|
||||
got, _ := f.FetchURL()
|
||||
if got := got.String(); got != want {
|
||||
t.Errorf("expected %q but got %q", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
11
src/feeds/item.go
Normal file
11
src/feeds/item.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package feeds
|
||||
|
||||
type Items []Item
|
||||
|
||||
type Item struct {
|
||||
Title string
|
||||
Link string
|
||||
Links []string
|
||||
Preview string
|
||||
Body string
|
||||
}
|
||||
491
src/feeds/testdata/slashdot.rss
vendored
Normal file
491
src/feeds/testdata/slashdot.rss
vendored
Normal file
@@ -0,0 +1,491 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
|
||||
<rdf:RDF
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://purl.org/rss/1.0/"
|
||||
xmlns:admin="http://webns.net/mvcb/"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
|
||||
xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"
|
||||
>
|
||||
|
||||
<channel rdf:about="https://slashdot.org/">
|
||||
<title>Slashdot</title>
|
||||
<link>https://slashdot.org/</link>
|
||||
<description>News for nerds, stuff that matters</description>
|
||||
<dc:language>en-us</dc:language>
|
||||
<dc:rights>Copyright Slashdot Media. All Rights Reserved.</dc:rights>
|
||||
<dc:date>2025-04-27T18:06:25+00:00</dc:date>
|
||||
<dc:publisher>Slashdot Media</dc:publisher>
|
||||
<dc:creator>feedback@slashdot.org</dc:creator>
|
||||
<dc:subject>Technology</dc:subject>
|
||||
<syn:updateBase>1970-01-01T00:00+00:00</syn:updateBase>
|
||||
<syn:updateFrequency>1</syn:updateFrequency>
|
||||
<syn:updatePeriod>hourly</syn:updatePeriod>
|
||||
<items>
|
||||
<rdf:Seq>
|
||||
<rdf:li rdf:resource="https://it.slashdot.org/story/25/04/27/088238/wsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://news.slashdot.org/story/25/04/26/238243/canadian-university-cancels-coding-competition-over-suspected-ai-cheating?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://linux.slashdot.org/story/25/04/27/0127203/lenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://entertainment.slashdot.org/story/25/04/27/040248/yoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://linux.slashdot.org/story/25/04/27/0547245/linus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://tech.slashdot.org/story/25/04/27/0252257/4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://tech.slashdot.org/story/25/04/27/0031222/ipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://science.slashdot.org/story/25/04/26/2217249/can-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://it.slashdot.org/story/25/04/26/2042230/read-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://entertainment.slashdot.org/story/25/04/26/1935238/youtube-is-huge---and-a-few-creators-are-getting-rich?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://mobile.slashdot.org/story/25/04/26/078214/can-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://news.slashdot.org/story/25/04/26/0625244/california-becomes-the-worlds-fourth-largest-economy-overtaking-japan?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://news.slashdot.org/story/25/04/26/0520221/us-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://slashdot.org/story/25/04/26/0742205/nyt-asks-should-we-start-taking-the-welfare-of-ai-seriously?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li rdf:resource="https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
</rdf:Seq>
|
||||
</items>
|
||||
<image rdf:resource="https://a.fsdn.com/sd/topics/topicslashdot.gif" />
|
||||
<textinput rdf:resource="https://slashdot.org/search.pl" />
|
||||
</channel>
|
||||
<image rdf:about="https://a.fsdn.com/sd/topics/topicslashdot.gif">
|
||||
<title>Slashdot</title>
|
||||
<url>https://a.fsdn.com/sd/topics/topicslashdot.gif</url>
|
||||
<link>https://slashdot.org/</link>
|
||||
</image>
|
||||
<item rdf:about="https://it.slashdot.org/story/25/04/27/088238/wsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>WSJ: Tech-Industry Workers Now 'Miserable', Fearing Layoffs, Working Longer Hours</title>
|
||||
<link>https://it.slashdot.org/story/25/04/27/088238/wsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"Not so long ago, working in tech meant job security, extravagant perks and a bring-your-whole-self-to-the-office ethos rare in other industries," writes the Wall Street Journal.
|
||||
|
||||
But now tech work "looks like a regular job," with workers "contending with the constant fear of layoffs, longer hours and an ever-growing list of responsibilities for the same pay."
|
||||
|
||||
Now employees find themselves doing the work of multiple laid-off colleagues. Some have lost jobs only to be rehired into positions that aren't eligible for raises or stock grants. Changing jobs used to be a surefire way to secure a raise; these days, asking for more money can lead to a job offer being withdrawn.
|
||||
|
||||
The shift in tech has been building slowly. For years, demand for workers outstripped supply, a dynamic that peaked during the Covid-19 pandemic. Big tech companies like Meta and Salesforce admitted they brought on too many employees. The ensuing downturn included mass layoffs that started in 2022...
|
||||
|
||||
[S]ome longtime tech employees say they no longer recognize the companies they work for. Management has become more focused on delivering the results Wall Street expects. Revenue remains strong for tech giants, but they're pouring resources into costly AI infrastructure, putting pressure on cash flow. With the industry all grown up, a heads-down, keep-quiet mentality has taken root, workers say... Tech workers are still well-paid compared with other sectors, but currently there's a split in the industry. Those working in AI &mdash; and especially those with Ph.D.s &mdash; are seeing their compensation packages soar. But those without AI experience are finding they're better off staying where they are, because companies aren't paying what they were a few years ago.
|
||||
|
||||
Other excepts from the Wall Street Journal's article:
|
||||
|
||||
"I'm hearing of people having 30 direct reports," says David Markley, who spent seven years at Amazon and is now an executive coach for workers at large tech companies. "It's not because the companies don't have the money. In a lot of ways, it's because of AI and the narratives out there about how collapsing the organization is better...."
|
||||
In some cases, companies post record revenue while still trimming head count.
|
||||
Google co-founder Sergey Brin told a group of employees in February that 60 hours a week was the sweet spot of productivity, in comments reported earlier by the New York Times.
|
||||
One recruiter at Meta who had been laid off by the company was rehired into her old role last year, but with a catch: She's now classified as a "short-term employee." Her contract is eligible for renewal, but she doesn't get merit pay increases, promotions or stock. The recruiter says she's responsible for a volume of work that used to be spread among several people. The company refers to being loaded with such additional responsibilities as "agility." More than 50,000 tech workers from over 100 companies have been laid off in 2025, according to Layoffs.fyi, a website that tracks job cuts and crowdsources lists of laid off workers...
|
||||
Even before those 50,000 layoffs in 2025,
|
||||
Silicon Valley's Mercury News was citing some interesting statistics from economic research/consulting firm Beacon Economics. In 2020, 2021 and 2022, the San Francisco Bay Area added 74,700 tech jobs But then in 2023 and 2024 the industry had slashed even more tech jobs -- 80,200 -- for a net loss (over five years) of 5,500.
|
||||
|
||||
So is there really a cutback in perks and a fear of layoffs that's casting a pall over the industry? share your own thoughts and experiences in the comments. Do you agree with the picture that's being painted by the Wall Street Journal?
|
||||
|
||||
They told their readers that tech workers are now "just like the rest of us: miserable at work."<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=WSJ%3A++Tech-Industry+Workers+Now+'Miserable'%2C+Fearing+Layoffs%2C+Working+Longer+Hours%3A+https%3A%2F%2Fit.slashdot.org%2Fstory%2F25%2F04%2F27%2F088238%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fit.slashdot.org%2Fstory%2F25%2F04%2F27%2F088238%2Fwsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://it.slashdot.org/story/25/04/27/088238/wsj-tech-industry-workers-now-miserable-fearing-layoffs-working-longer-hours?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676561&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T16:34:00+00:00</dc:date>
|
||||
<dc:subject>it</dc:subject>
|
||||
<slash:department>misery-loves-company</slash:department>
|
||||
<slash:section>it</slash:section>
|
||||
<slash:comments>22</slash:comments>
|
||||
<slash:hit_parade>22,22,18,16,3,2,1</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://news.slashdot.org/story/25/04/26/238243/canadian-university-cancels-coding-competition-over-suspected-ai-cheating?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Canadian University Cancels Coding Competition Over Suspected AI Cheating</title>
|
||||
<link>https://news.slashdot.org/story/25/04/26/238243/canadian-university-cancels-coding-competition-over-suspected-ai-cheating?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>The university blamed it on "the significant number of students" who violated their coding competition's rules.
|
||||
Long-time Slashdot reader theodp quotes this report from The Logic: Finding that many students violated rules and submitted code not written by themselves, the University of Waterloo's Centre for Computing and Math decided not to release results from its annual Canadian Computing Competition (CCC), which many students rely on to bolster their chances of being accepted into Waterloo's prestigious computing and engineering programs, or land a spot on teams to represent Canada in international competitions. "It is clear that many students submitted code that they did not write themselves, relying instead on forbidden external help," the CCC co-chairs explained in a statement. "As such, the reliability of 'ranking' students would neither be equitable, fair, or accurate."
|
||||
|
||||
"It is disappointing that the students who violated the CCC Rules will impact those students who are deserving of recognition," the univeresity said in its statement. They added that they are "considering possible ways to address this problem for future contests."<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Canadian+University+Cancels+Coding+Competition+Over+Suspected+AI+Cheating%3A+https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F238243%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F238243%2Fcanadian-university-cancels-coding-competition-over-suspected-ai-cheating%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://news.slashdot.org/story/25/04/26/238243/canadian-university-cancels-coding-competition-over-suspected-ai-cheating?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676353&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T15:34:00+00:00</dc:date>
|
||||
<dc:subject>education</dc:subject>
|
||||
<slash:department>halt-and-catch-fire</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>10</slash:comments>
|
||||
<slash:hit_parade>10,10,9,8,2,1,0</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://linux.slashdot.org/story/25/04/27/0127203/lenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Lenovo May Be Avoiding the 'Windows Tax' By Offering Cheaper Laptops With Pre-Installed Linux</title>
|
||||
<link>https://linux.slashdot.org/story/25/04/27/0127203/lenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"The U.S. and Canadian websites for Lenovo offered U.S. $140 and CAD $211 off on the same ThinkPad X1 Carbon model when choosing any one of the Linux-based alternatives," reports It's FOSS News:
|
||||
|
||||
|
||||
This was brought to my attention thanks to a Reddit post... Others then chimed in, saying that Lenovo has been doing this since at least 2020 and that the big price difference shows how ridiculous Windows' pricing is...
|
||||
Not all models from their laptop lineup, like ThinkPad, Yoga, Legion, LOQ, etc., feature an option to get Linux pre-installed during the checkout process. Luckily, there is an easy way to filter through the numerous laptops. Just go to the laptops section (U.S.) on the Lenovo website and turn on the "Operating System" filter under the Filter by specs sidebar menu.
|
||||
|
||||
The article end with an embedded YouTube video showing a VCR playing a videotape of a 1999 local TV news report... about the legendary "Windows Refund Day" protests.
|
||||
|
||||
Slashdot ran numerous stories about the event &mdash; including one by Jon Katz...<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Lenovo+May+Be+Avoiding+the+'Windows+Tax'+By+Offering+Cheaper+Laptops+With+Pre-Installed+Linux%3A+https%3A%2F%2Flinux.slashdot.org%2Fstory%2F25%2F04%2F27%2F0127203%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Flinux.slashdot.org%2Fstory%2F25%2F04%2F27%2F0127203%2Flenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://linux.slashdot.org/story/25/04/27/0127203/lenovo-may-be-avoiding-the-windows-tax-by-offering-cheaper-laptops-with-pre-installed-linux?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676403&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T14:34:00+00:00</dc:date>
|
||||
<dc:subject>portables</dc:subject>
|
||||
<slash:department>Windows-refund-day</slash:department>
|
||||
<slash:section>linux</slash:section>
|
||||
<slash:comments>23</slash:comments>
|
||||
<slash:hit_parade>23,23,22,19,3,1,1</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://entertainment.slashdot.org/story/25/04/27/040248/yoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Yoda Bloopers Released - and George Lucas Reveals Why Yoda Talks Backwards</title>
|
||||
<link>https://entertainment.slashdot.org/story/25/04/27/040248/yoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>80-year-old George Lucas appeared this week at a 45th anniversary screening of The Empire Strikes Back, reports CNN &mdash; and finally gave a good explanation for why Yoda speaks the way he does. "He explained that it came about in order to ensure that the little alien's usually profound messages really landed with audiences."
|
||||
"Because if you speak regular English, people won't listen that much," Lucas said at the 2025 TCM Classic Film Festival, per Variety . "But if he had an accent, or it's really hard to understand what he's saying, they focus on what he's saying." Yoda was "basically the philosopher of the movie," the filmmaker added. "I had to figure out a way to get people to actually listen &mdash; especially 12-year-olds."
|
||||
|
||||
Also this week, the verified Instagram accounts for Disney+, Star Wars and LucasFilm &mdash; Lucas' film and television production company &mdash; posted clips of Yoda doing bloopers on the set of "Star Wars" films, with [Frank] Oz continuing to do the voice and manipulate the heavy Yoda puppet even on takes that were unusable. Suffice it to say: One for the ages, Yoda is.
|
||||
|
||||
Lucas also remembered how he'd "mounted a guerilla campaign to generate excitement" for the first Star Wars movie, reports Variety. ("I got the kids walking around Disneyland and the Comic Cons and all that kind of stuff... that's why Fox was so shocked when the first day the lines were all around the block.") And Variety says Lucas described a condition in his contract for Star Wars "that would again be life-changing, both for him and the entertainment industry as a whole."
|
||||
|
||||
"I said, 'besides that, I'd like licensing.' They went, 'What's licensing?'" Unimpressed by the film, and colored by the history of movie merchandising to that point, the studio capitulated to his demands. "They talked to themselves, and they went, 'He's never going to be able to do that. It takes them a billion dollars and a year to make a toy or make anything. There's no money in that at all.'"<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Yoda+Bloopers+Released+-+and+George+Lucas+Reveals+Why+Yoda+Talks+Backwards%3A+https%3A%2F%2Fentertainment.slashdot.org%2Fstory%2F25%2F04%2F27%2F040248%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fentertainment.slashdot.org%2Fstory%2F25%2F04%2F27%2F040248%2Fyoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://entertainment.slashdot.org/story/25/04/27/040248/yoda-bloopers-released---and-george-lucas-reveals-why-yoda-talks-backwards?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676461&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T11:34:00+00:00</dc:date>
|
||||
<dc:subject>starwars</dc:subject>
|
||||
<slash:department>do-or-do-not-do</slash:department>
|
||||
<slash:section>entertainment</slash:section>
|
||||
<slash:comments>19</slash:comments>
|
||||
<slash:hit_parade>19,19,14,13,1,0,0</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://linux.slashdot.org/story/25/04/27/0547245/linus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Linus Torvalds Expresses His Hatred For Case-Insensitive File-Systems</title>
|
||||
<link>https://linux.slashdot.org/story/25/04/27/0547245/linus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>Some patches for Linux 6.15-rc4 (updating the kernel driver for the Bcachefs file system) triggered some "straight-to-the-point wisdom" from Linus Torvalds about case-insensitive filesystems, reports Phoronix.
|
||||
|
||||
Bcachefs developer Kent Overstreet started the conversation, explaining how some buggy patches for their case-insensitive file and folder support were upstreamed into the Bcachefs kernel driver nearly two years ago:
|
||||
|
||||
When I was discussing with the developer who did the implementation, I noted that fstests should already have tests. However, it seems I neglected to tell him to make sure the tests actually run... It is _not_ enough to simply rely on the automated tests. You have to have eyes on what your code is doing.
|
||||
Overstreet added "There's a story behind the case insensitive directory fixes, and lessons to be learned." To which Torvalds replied.... "No."
|
||||
"The only lesson to be learned is that filesystem people never learn."
|
||||
|
||||
|
||||
|
||||
Torvalds: Case-insensitive names are horribly wrong, and you shouldn't have done them at all. The problem wasn't the lack of testing, the problem was implementing it in the first place. The problem is then compounded by "trying to do it right", and in the process doing it horrible wrong indeed, because "right" doesn't exist, but trying to will make random bytes have very magical meaning.
|
||||
|
||||
And btw, the tests are all completely broken anyway. Last I saw, they didn't actually test for all the really interesting cases &mdash; the ones that cause security issues in user land. Security issues like "user space checked that the filename didn't match some security-sensitive pattern". And then the shit-for-brains filesystem ends up matching that pattern *anyway*, because the people who do case insensitivity *INVARIABLY* do things like ignore non-printing characters, so now "case insensitive" also means "insensitive to other things too"....
|
||||
Dammit. Case sensitivity is a BUG. The fact that filesystem people *still* think it's a feature, I cannot understand. It's like they revere the old FAT filesystem _so_ much that they have to recreate it &mdash; badly.
|
||||
|
||||
And this led to a very lively back-and-forth discussion.
|
||||
|
||||
Slashdot's summary of the highlights:<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Linus+Torvalds+Expresses+His+Hatred+For+Case-Insensitive+File-Systems%3A+https%3A%2F%2Flinux.slashdot.org%2Fstory%2F25%2F04%2F27%2F0547245%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Flinux.slashdot.org%2Fstory%2F25%2F04%2F27%2F0547245%2Flinus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://linux.slashdot.org/story/25/04/27/0547245/linus-torvalds-expresses-his-hatred-for-case-insensitive-file-systems?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676497&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T07:34:00+00:00</dc:date>
|
||||
<dc:subject>linux</dc:subject>
|
||||
<slash:department>insensitivity-training</slash:department>
|
||||
<slash:section>linux</slash:section>
|
||||
<slash:comments>159</slash:comments>
|
||||
<slash:hit_parade>159,159,155,138,33,21,14</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://tech.slashdot.org/story/25/04/27/0252257/4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>4chan Returns, Details Breach, Blames Funding Issues, Ends Shockwave Board</title>
|
||||
<link>https://tech.slashdot.org/story/25/04/27/0252257/4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"4chan, down for more than a week after hackers got in through an insecure script that handled PDFs, is back online," notes BoingBoing. (They add that Thursday saw 4chan's first blog postin years &mdash; just the words "Testing testing 123 123...") But 4chan posted a much longer explanation on Friday," confirming their servers were compromised by a malicious PDF upload from "a hacker using a UK IP address," granting access to their databases and administrative dashboard.
|
||||
|
||||
|
||||
The attacker "spent several hours exfiltrating database tables and much of 4chan's source code. When they had finished downloading what they wanted, they began to vandalize 4chan at which point moderators became aware and 4chan's servers were halted, preventing further access."
|
||||
|
||||
While not all of our servers were breached, the most important one was, and it was due to simply not updating old operating systems and code in a timely fashion. Ultimately this problem was caused by having insufficient skilled man-hours available to update our code and infrastructure, and being starved of money for years by advertisers, payment providers, and service providers who had succumbed to external pressure campaigns. We had begun a process of speccing new servers in late 2023. As many have suspected, until that time 4chan had been running on a set of servers purchased second-hand by moot a few weeks before his final Q&amp;A [in 2015], as prior to then we simply were not in a financial position to consider such a large purchase. Advertisers and payment providers willing to work with 4chan are rare, and are quickly pressured by activists into cancelling their services. Putting together the money for new equipment took nearly a decade...
|
||||
|
||||
The free time that 4chan's development team had available to dedicate to 4chan was insufficient to update our software and infrastructure fast enough, and our luck ran out. However, we have not been idle during our nearly two weeks of downtime. The server that was breached has been replaced, with the operating system and code updated to the latest versions. PDF uploads have been temporarily disabled on those boards that supported them, but they will be back in the near future. One slow but much beloved board, /f/ &mdash; Flash, will not be returning however, as there is no realistic way to prevent similar exploits using .swf files.
|
||||
|
||||
We are bringing on additional volunteer developers to help keep up with the workload, and our team of volunteer janitors &amp; moderators remains united despite the grievous violations some have suffered to their personal privacy.
|
||||
4chan is back. No other website can replace it, or this community. No matter how hard it is, we are not giving up.
|
||||
|
||||
<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=4chan+Returns%2C+Details+Breach%2C+Blames+Funding+Issues%2C+Ends+Shockwave+Board%3A+https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F27%2F0252257%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F27%2F0252257%2F4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://tech.slashdot.org/story/25/04/27/0252257/4chan-returns-details-breach-blames-funding-issues-ends-shockwave-board?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676441&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T04:34:00+00:00</dc:date>
|
||||
<dc:subject>social</dc:subject>
|
||||
<slash:department>artistic-works-of-fiction</slash:department>
|
||||
<slash:section>technology</slash:section>
|
||||
<slash:comments>40</slash:comments>
|
||||
<slash:hit_parade>40,35,30,24,7,3,1</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://tech.slashdot.org/story/25/04/27/0031222/ipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>iPad Jammed in Seat Forces Emergency Landing of Airplane Carrying 400 Passengers</title>
|
||||
<link>https://tech.slashdot.org/story/25/04/27/0031222/ipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>An anonymous reader shared this report from Business Insider:
|
||||
|
||||
A Lufthansa flight carrying 461 passengers had to divert after someone's tablet became "jammed" in a business-class seat.
|
||||
|
||||
The Airbus A380 took off from Los Angeles on Wednesday, bound for Munich, and had been flying for around three hours when the pilots diverted to Boston Logan International Airport. In a statement to Business Insider, an airline spokesperson said the tablet had become "jammed in a Business Class seat" and had "already shown visible signs of deformation due to the seat's movements" when the flight diverted. [The aviation site] Simply Flying, which first reported the news, said the device was an iPad.
|
||||
|
||||
The decision to divert was taken "to eliminate any potential risk, particularly with regard to possible overheating," the spokesperson added, saying that it was the joint decision of the crew and air traffic control. Lithium batteries pose a safety risk if damaged, punctured, or crushed... In a confined space like an aircraft cabin, a lithium battery fire poses a serious hazard to the passengers onboard. Last year, a Breeze Airways flight from Los Angeles to Pittsburgh had to make an emergency landing in Albuquerque after a passenger's laptop caught fire.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=iPad+Jammed+in+Seat+Forces+Emergency+Landing+of+Airplane+Carrying+400+Passengers%3A+https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F27%2F0031222%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F27%2F0031222%2Fipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://tech.slashdot.org/story/25/04/27/0031222/ipad-jammed-in-seat-forces-emergency-landing-of-airplane-carrying-400-passengers?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676377&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-27T01:34:00+00:00</dc:date>
|
||||
<dc:subject>transportation</dc:subject>
|
||||
<slash:department>battery-low</slash:department>
|
||||
<slash:section>technology</slash:section>
|
||||
<slash:comments>56</slash:comments>
|
||||
<slash:hit_parade>56,53,45,38,9,3,2</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://science.slashdot.org/story/25/04/26/2217249/can-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Can Solar Wind Make Water on the Moon? A NASA Experiment Shows Maybe </title>
|
||||
<link>https://science.slashdot.org/story/25/04/26/2217249/can-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"Future moon astronauts may find water more accessible than previously thought," writes Space.com, citing a new NASA-led experiment:
|
||||
|
||||
Because the moon lacks a magnetic field like Earth's, the barren lunar surface is constantly bombarded by energetic particles from the sun... Li Hsia Yeo, a planetary scientist at NASA's Goddard Space Flight Center in Maryland, led a lab experiment observing the effects of simulated solar wind on two samples of loose regolith brought to Earth by the Apollo 17 mission... To mimic conditions on the moon, the researchers built a custom apparatus that included a vacuum chamber, where the samples were placed, and a tiny particle accelerator, which the scientists used to bombard the samples with hydrogen ions for several days.
|
||||
|
||||
"The exciting thing here is that with only lunar soil and a basic ingredient from the sun &mdash; which is always spitting out hydrogen &mdash; there's a possibility of creating water," Yeo said in a statement. "That's incredible to think about." Supporting this idea, observations from previous moon missions have revealed an abundance of hydrogen gas in the moon's tenuous atmosphere. Scientists suspect that solar-wind-driven heating facilitates the combination of hydrogen atoms on the surface into hydrogen gas, which then escapes into space. This process also has a surprising upside, the new study suggests. Leftover oxygen atoms are free to bond with new hydrogen atoms formed by repeated bombardment of the solar wind, prepping the moon for more water formation on a renewable basis.
|
||||
|
||||
The findings could help assess how sustainable water on the moon is, as the sought-after resource is crucial for both life support and as propellant for rockets. The team's study was published in March in the journal JGR Planets .
|
||||
|
||||
NASA created a fascinating animation showing how water is released from the Moon during meteor showers. (In 2016 scientists discovered that when speck of comet debris vaporize on impact, they create shock waves in the lunar soil which can sometimes breach the dry upper layer, releasing water molecules from the hydrated layer below...)<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Can+Solar+Wind+Make+Water+on+the+Moon%3F+A+NASA+Experiment+Shows+Maybe+%3A+https%3A%2F%2Fscience.slashdot.org%2Fstory%2F25%2F04%2F26%2F2217249%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fscience.slashdot.org%2Fstory%2F25%2F04%2F26%2F2217249%2Fcan-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://science.slashdot.org/story/25/04/26/2217249/can-solar-wind-make-water-on-the-moon-a-nasa-experiment-shows-maybe?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676331&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T22:34:00+00:00</dc:date>
|
||||
<dc:subject>moon</dc:subject>
|
||||
<slash:department>moonshining</slash:department>
|
||||
<slash:section>science</slash:section>
|
||||
<slash:comments>19</slash:comments>
|
||||
<slash:hit_parade>19,17,15,13,4,4,3</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://it.slashdot.org/story/25/04/26/2042230/read-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>'Read the Manual': Misconfigured Google Analytics Led to a Data Breach Affecting 4.7M</title>
|
||||
<link>https://it.slashdot.org/story/25/04/26/2042230/read-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>Slashdot reader itwbennett writes: Personal health information on 4.7 million Blue Shield California subscribers was unintentionally shared between Google Analytics and Google Ads between April 2021 and January 2025 due to a misconfiguration error. Security consultant and SANS Institute instructor Brandon Evans points to two lessons to take from this debacle:
|
||||
|
||||
Read the documentation of any third party service you sign up for, to understand the security and privacy controls;Know what data is being collected from your organization, and what you don't want shared.
|
||||
|
||||
"If there is a concern by the organization that Google Ads would use this information, they should really consider whether or not they should be using a platform like Google Analytics in the first place," Evans says in the article. "Because from a technical perspective, there is nothing stopping Google from sharing the information across its platform...
|
||||
|
||||
"Google definitely gives you a great bunch of controls, but technically speaking, that data is within the walls of that organization, and it's impossible to know from the outside how that data is being used."<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status='Read+the+Manual'%3A+Misconfigured+Google+Analytics+Led+to+a+Data+Breach+Affecting+4.7M%3A+https%3A%2F%2Fit.slashdot.org%2Fstory%2F25%2F04%2F26%2F2042230%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fit.slashdot.org%2Fstory%2F25%2F04%2F26%2F2042230%2Fread-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://it.slashdot.org/story/25/04/26/2042230/read-the-manual-misconfigured-google-analytics-led-to-a-data-breach-affecting-47m?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676287&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T21:34:00+00:00</dc:date>
|
||||
<dc:subject>google</dc:subject>
|
||||
<slash:department>feeling-insecure</slash:department>
|
||||
<slash:section>it</slash:section>
|
||||
<slash:comments>14</slash:comments>
|
||||
<slash:hit_parade>14,14,10,9,3,2,2</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://entertainment.slashdot.org/story/25/04/26/1935238/youtube-is-huge---and-a-few-creators-are-getting-rich?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>YouTube is Huge - and a Few Creators Are Getting Rich</title>
|
||||
<link>https://entertainment.slashdot.org/story/25/04/26/1935238/youtube-is-huge---and-a-few-creators-are-getting-rich?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"Google-owned YouTube's revenue last year was estimated to be $54.2 billion," reports the Los Angeles Times, "which would make it the second-largest media company behind Walt Disney Co., according to a recent report from research firm MoffettNathanson, which called YouTube 'the new king of all media.'"
|
||||
|
||||
YouTube, run by Chief Executive Neal Mohan since 2023, accounted for 12% of U.S. TV viewing in March, more than other rival streaming platforms including Netflix and Tubi, according to Nielsen... More people are watching YouTube on TV sets rather than on smartphones and computer screens, consuming more than 1 billion hours on average of YouTube content on TV daily, the company said on its website.
|
||||
|
||||
When YouTube first started its founders envisioned it as a dating site, according to the article, "where people would upload videos and score them. When that didn't work, the founders decided to open up the platform for all sorts of videos." And since this was 20 years ago, "Users drove traffic to YouTube by sharing videos on MySpace."
|
||||
|
||||
But the article includes stories of people getting rich through YouTube's sharing of ad revenue:
|
||||
|
||||
Patrick Starrr, who produces makeup tutorial videos, said he made his first $1 million through YouTube at the age of 25. He left his job at retailer MAC Cosmetics in Florida and moved to L.A...
|
||||
|
||||
[Video creator Dhar Mann] started posting videos on YouTube in 2018 with no film background. Mann previously had a business that sold supplies to grow weed. Today, his company, Burbank-based Dhar Mann Studios, operates on 125,000 square feet of production space, employs roughly 200 people and works with 2,000 actors a year on family friendly programs that touch on how students and families deal with topics such as bullying, narcolepsy, chronic inflammatory bowel disease and hoarding. Mann made $45 million last year, according to Forbes estimates. The majority of his company's revenue comes through YouTube.
|
||||
He tells the Times "I don't think it's just the future of TV &mdash; it is TV, and the world is catching on."
|
||||
|
||||
And then there's this...
|
||||
|
||||
"My mom would always give me so much crap about it &mdash; she would say, 'Why do you want to do YouTube?'" said Chucky Appleby, now an executive at MrBeast. His reply: "Mom, you can make a living from this." MrBeast's holding company, Beast Industries, which employs more than 400 people, made $473 million in revenue last year, according to Business Insider. In the last 28 days, MrBeast content &mdash; which includes challenges and stunt videos &mdash; received 3.6 billion views on YouTube, Appleby said.
|
||||
|
||||
Appleby, 28, said he's since bought a Jeep for his mom.
|
||||
<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=YouTube+is+Huge+-+and+a+Few+Creators+Are+Getting+Rich%3A+https%3A%2F%2Fentertainment.slashdot.org%2Fstory%2F25%2F04%2F26%2F1935238%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fentertainment.slashdot.org%2Fstory%2F25%2F04%2F26%2F1935238%2Fyoutube-is-huge---and-a-few-creators-are-getting-rich%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://entertainment.slashdot.org/story/25/04/26/1935238/youtube-is-huge---and-a-few-creators-are-getting-rich?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23676255&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T20:34:00+00:00</dc:date>
|
||||
<dc:subject>tv</dc:subject>
|
||||
<slash:department>video-killed-the-radio-star</slash:department>
|
||||
<slash:section>entertainment</slash:section>
|
||||
<slash:comments>27</slash:comments>
|
||||
<slash:hit_parade>27,26,22,19,9,5,3</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://mobile.slashdot.org/story/25/04/26/078214/can-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Can a New 'Dumbphone' With an E Ink Display Help Rewire Your Brain?</title>
|
||||
<link>https://mobile.slashdot.org/story/25/04/26/078214/can-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>ZDNet's reviewer says "I tested this affordable E Ink phone for two weeks, and it rewired my brain (for the better)."
|
||||
|
||||
It's Mudita's new Kompakt smartphone with a two-color E Ink display &mdash; which ZDNet calls "an affordable choice" for those "considering investing in a so-called 'dumbphone'..."
|
||||
|
||||
|
||||
Compared to modern smartphones, the Mudita Kompakt is a bit chunky at half an inch thick and five inches long. It's still rather light, though, weighing just 164 grams and covered in soft touch material, so it feels good in the hand. The bezels around the 4.3-inch display are rather large, with three touch-sensitive buttons for back, home, and quick settings, so navigating to key elements is intuitive, whether you're coming from Android or iOS.
|
||||
The phone features a fingerprint sensor to lock and unlock, and it's housed on the power button in the middle of the right side. I'm a huge fan of consolidating these two purposes to the same button, and it works flawlessly.... You can charge via the USB-C, but surprisingly, it also supports wireless charging. All in all, the battery is quite good. Mudita says it can last for up to six days on standby, with around two days of standard use. In my testing, I found this to be about accurate.
|
||||
|
||||
On the left side of the device is a button that houses one of its key features: offline mode. Switching to this mode disables all wireless connectivity and support for the camera, so it truly becomes distraction-free.. [T]here is undoubtedly some lag in certain apps &mdash; such as the camera &mdash; due to the E Ink display technology and processor/RAM specifications. You will also likely notice some lag in text messaging if you tap quickly on the keyboard, often resulting in getting ahead of the spell-checking feature. As far as apps go, in addition to phone calls and text messages, the Kompakt includes an alarm, calculator, chess game, maps, meditation, weather, and a voice recorder.
|
||||
|
||||
Phone calls "sounded great on both ends," according to the review. (And text messaging "works well if you don't tap too quickly on the keyboard.") But the 8MP camera produced photos "that look like they were taken over ten years ago." (And accessing the internal storage "requires connecting to a Windows PC and launching File Explorer," although "you can also just share photos via text messaging, as it's much faster than using a computer.") But ZDNet calls it an "attractive &mdash; if very simplified &mdash; E Ink display."
|
||||
|
||||
Mudita is asking $369 now for preorder customers, according to the article, while the phone will be available in May for $439.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Can+a+New+'Dumbphone'+With+an+E+Ink+Display+Help+Rewire+Your+Brain%3F%3A+https%3A%2F%2Fmobile.slashdot.org%2Fstory%2F25%2F04%2F26%2F078214%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fmobile.slashdot.org%2Fstory%2F25%2F04%2F26%2F078214%2Fcan-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://mobile.slashdot.org/story/25/04/26/078214/can-a-new-dumbphone-with-an-e-ink-display-help-rewire-your-brain?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675857&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T19:34:00+00:00</dc:date>
|
||||
<dc:subject>cellphones</dc:subject>
|
||||
<slash:department>phoning-it-in</slash:department>
|
||||
<slash:section>mobile</slash:section>
|
||||
<slash:comments>85</slash:comments>
|
||||
<slash:hit_parade>85,84,71,65,11,5,1</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://news.slashdot.org/story/25/04/26/0625244/california-becomes-the-worlds-fourth-largest-economy-overtaking-japan?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>California Becomes the World's Fourth-Largest Economy, Overtaking Japan</title>
|
||||
<link>https://news.slashdot.org/story/25/04/26/0625244/california-becomes-the-worlds-fourth-largest-economy-overtaking-japan?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>"Only the United States, China and Germany have larger economies than California," reports CNN.
|
||||
|
||||
In fact, they add that California "outpaced all three countries with growth of 6% last year," according to the California governor's office (which cites new data from the International Monetary Fund and the U.S. Bureau of Economic Analysis):
|
||||
|
||||
|
||||
In 2024, California's growth rate of 6% outpaced the top three economies: U.S. (5.3%), China (2.6%) and Germany (2.9%)...
|
||||
|
||||
|
||||
With an increasing state population and recent record-high tourism spending, California is the nation's top state for new business starts, access to venture capital funding, and manufacturing, high-tech, and agriculture. The state drives national economic growth and also sends over $83 billion more to the federal government than it receives in federal funding. California is the leading agricultural producer in the country and is also the center for manufacturing output in the United States, with over 36,000 manufacturing firms employing over 1.1 million Californians.
|
||||
|
||||
|
||||
The data shows that last year California accounted for 14% of America's GDP, CNN points out, "driven by Silicon Valley and its real estate and finance sectors."<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=California+Becomes+the+World's+Fourth-Largest+Economy%2C+Overtaking+Japan%3A+https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F0625244%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F0625244%2Fcalifornia-becomes-the-worlds-fourth-largest-economy-overtaking-japan%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://news.slashdot.org/story/25/04/26/0625244/california-becomes-the-worlds-fourth-largest-economy-overtaking-japan?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675837&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T18:34:00+00:00</dc:date>
|
||||
<dc:subject>usa</dc:subject>
|
||||
<slash:department>gold-rush</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>127</slash:comments>
|
||||
<slash:hit_parade>127,121,101,87,20,9,4</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://news.slashdot.org/story/25/04/26/0520221/us-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>US Attorney for D.C. Accuses Wikipedia of 'Propaganda', Threatens Nonprofit Status</title>
|
||||
<link>https://news.slashdot.org/story/25/04/26/0520221/us-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>An anonymous reader shared this report from the Washington Post:
|
||||
|
||||
|
||||
The acting U.S. attorney for the District of Columbia sent a letter to the nonprofit that runs Wikipedia, accusing the tax-exempt organization of "allowing foreign actors to manipulate information and spread propaganda to the American public."
|
||||
|
||||
|
||||
In the letter dated April 24, Ed Martin said he sought to determine whether the Wikimedia Foundation's behavior is in violation of its Section 501(c)(3) status. Martin asked the foundation to provide detailed information about its editorial process, its trust and safety measures, and how it protects its information from foreign actors. "Wikipedia is permitting information manipulation on its platform, including the rewriting of key, historical events and biographical information of current and previous American leaders, as well as other matters implicating the national security and the interests of the United States," Martin wrote. "Masking propaganda that influences public opinion under the guise of providing informational material is antithetical to Wikimedia's 'educational' mission."
|
||||
Google prioritizes Wikipedia articles, the letter points out, which "will only amplify propaganda" if the content contained in Wikipedia articles "is biased, unreliable, or sourced by entities who wish to do harm to the United States." And as a U.S.-based non-profit, Wikipedia enjoys tax-exempt status while its board "is composed primarily of foreign nationals," the letter argues, "subverting the interests of American taxpayers."
|
||||
|
||||
While noting Martin's concerns about "allowing foreign actors to manipulate information and spread propaganda," the Washington Post also notes that before being named U.S. attorney, "Martin appeared on Russia-backed media networks more than 150 times, The Washington Post reported last week...."
|
||||
Additional articles about the letter here and here.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=US+Attorney+for+D.C.+Accuses+Wikipedia+of+'Propaganda'%2C+Threatens+Nonprofit+Status%3A+https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F0520221%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F25%2F04%2F26%2F0520221%2Fus-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://news.slashdot.org/story/25/04/26/0520221/us-attorney-for-dc-accuses-wikipedia-of-propaganda-threatens-nonprofit-status?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675803&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T17:34:00+00:00</dc:date>
|
||||
<dc:subject>usa</dc:subject>
|
||||
<slash:department>burn-it-to-the-wiki</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>155</slash:comments>
|
||||
<slash:hit_parade>155,151,126,105,38,28,23</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://slashdot.org/story/25/04/26/0742205/nyt-asks-should-we-start-taking-the-welfare-of-ai-seriously?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>NYT Asks: Should We Start Taking the Welfare of AI Seriously?</title>
|
||||
<link>https://slashdot.org/story/25/04/26/0742205/nyt-asks-should-we-start-taking-the-welfare-of-ai-seriously?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>A New York Times technology columnist has a question.
|
||||
"Is there any threshold at which an A.I. would start to deserve, if not human-level rights, at least the same moral consideration we give to animals?"
|
||||
|
||||
|
||||
[W]hen I heard that researchers at Anthropic, the AI company that made the Claude chatbot, were starting to study "model welfare" &mdash; the idea that AI models might soon become conscious and deserve some kind of moral status &mdash; the humanist in me thought: Who cares about the chatbots? Aren't we supposed to be worried about AI mistreating us, not us mistreating it...?
|
||||
|
||||
But I was intrigued... There is a small body of academic research on A.I. model welfare, and a modest but growing number of experts in fields like philosophy and neuroscience are taking the prospect of A.I. consciousness more seriously, as A.I. systems grow more intelligent.... Tech companies are starting to talk about it more, too. Google recently posted a job listing for a "post-AGI" research scientist whose areas of focus will include "machine consciousness." And last year, Anthropic hired its first AI welfare researcher, Kyle Fish... [who] believes that in the next few years, as AI models develop more humanlike abilities, AI companies will need to take the possibility of consciousness more seriously....
|
||||
|
||||
|
||||
Fish isn't the only person at Anthropic thinking about AI welfare. There's an active channel on the company's Slack messaging system called #model-welfare, where employees check in on Claude's well-being and share examples of AI systems acting in humanlike ways. Jared Kaplan, Anthropic's chief science officer, said in a separate interview that he thought it was "pretty reasonable" to study AI welfare, given how intelligent the models are getting. But testing AI systems for consciousness is hard, Kaplan warned, because they're such good mimics. If you prompt Claude or ChatGPT to talk about its feelings, it might give you a compelling response. That doesn't mean the chatbot actually has feelings &mdash; only that it knows how to talk about them...
|
||||
|
||||
[Fish] said there were things that AI companies could do to take their models' welfare into account, in case they do become conscious someday. One question Anthropic is exploring, he said, is whether future AI models should be given the ability to stop chatting with an annoying or abusive user if they find the user's requests too distressing.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=NYT+Asks%3A+Should+We+Start+Taking+the+Welfare+of+AI+Seriously%3F%3A+https%3A%2F%2Fslashdot.org%2Fstory%2F25%2F04%2F26%2F0742205%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fslashdot.org%2Fstory%2F25%2F04%2F26%2F0742205%2Fnyt-asks-should-we-start-taking-the-welfare-of-ai-seriously%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://slashdot.org/story/25/04/26/0742205/nyt-asks-should-we-start-taking-the-welfare-of-ai-seriously?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675877&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T16:34:00+00:00</dc:date>
|
||||
<dc:subject>ai</dc:subject>
|
||||
<slash:department>soul-of-a-new-machine</slash:department>
|
||||
<slash:section>slashdot</slash:section>
|
||||
<slash:comments>91</slash:comments>
|
||||
<slash:hit_parade>91,90,86,71,16,7,4</slash:hit_parade>
|
||||
</item>
|
||||
<item rdf:about="https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Cheap 'Transforming' Electric Truck Announced by Jeff Bezos-Backed Startup</title>
|
||||
<link>https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0mainlinkanon&utm_medium=feed</link>
|
||||
<description>It's a pickup truck "that can change into whatever you need it to be &mdash; even an SUV," according to the manufacturer's web site.
|
||||
|
||||
Selling in America for just $20,000 (after federal incentives), the new electric truck is "affordable, deeply customizable, and very analog," says TechCrunch. "It has manual windows and it doesn't come with a main infotainment screen. Heck, it isn't even painted..."
|
||||
|
||||
Slate Auto is instead playing up the idea of wrapping its vehicles, something executives said they will sell in kits. Buyers can either have Slate do that work for them, or put the wraps on themselves. This not only adds to the idea of a buyer being able to personalize their vehicle, but it also cuts out a huge cost center for the company. It means Slate won't need a paint shop at its factory, allowing it to spend less to get to market, while also avoiding one of the most heavily regulated parts of vehicle manufacturing.
|
||||
|
||||
Slate is telling customers that they can name the car whatever they want, offering the ability to purchase an embossed wrap for the tailgate. Otherwise, the truck is just referred to as the "Blank Slate...." It's billing the add-ons as "easy DIY" that "non-gearheads" can tackle, and says it will launch a suite of how-to resources under the billing of Slate University... The early library of customizations on Slate's website range from functional to cosmetic. Buyers can add infotainment screens, speakers, roof racks, light covers, and much more.... All that said, Slate's truck comes standard with some federally mandated safety features such as automatic emergency braking, airbags, and a backup camera.
|
||||
"The specs show a maximum range of 150 miles on a single charge, with the option for a longer-range battery pack that could offer up to 240 miles," reports NBC News (adding that the vehicles "aren't expected to be delivered to customers until late 2026, but can be reserved for a refundable $50 fee.")
|
||||
|
||||
Earlier this month, TechCrunch broke the news that Bezos, along with the controlling owner of the Los Angeles Dodgers, Mark Walter; and a third investor, Thomas Tull, had helped Slate raise $111 million for the project. A document filed with the Securities and Exchange Commission listed Melinda Lewison, the head of Bezos' family office, as a Slate Auto director.
|
||||
|
||||
Thanks to Slashdot reader fjo3 for sharing the news.<p><div class="share_submission" style="position:relative;">
|
||||
<a class="slashpop" href="http://twitter.com/home?status=Cheap+'Transforming'+Electric+Truck+Announced+by+Jeff+Bezos-Backed+Startup%3A+https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F26%2F0425259%2F%3Futm_source%3Dtwitter%26utm_medium%3Dtwitter"><img src="https://a.fsdn.com/sd/twitter_icon_large.png"></a>
|
||||
<a class="slashpop" href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Ftech.slashdot.org%2Fstory%2F25%2F04%2F26%2F0425259%2Fcheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
|
||||
</div></p><p><a href="https://tech.slashdot.org/story/25/04/26/0425259/cheap-transforming-electric-truck-announced-by-jeff-bezos-backed-startup?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read more of this story</a> at Slashdot.</p><iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=23675777&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe></description>
|
||||
<dc:creator>EditorDavid</dc:creator>
|
||||
<dc:date>2025-04-26T15:34:00+00:00</dc:date>
|
||||
<dc:subject>transportation</dc:subject>
|
||||
<slash:department>keep-on-truckin'</slash:department>
|
||||
<slash:section>technology</slash:section>
|
||||
<slash:comments>140</slash:comments>
|
||||
<slash:hit_parade>140,140,135,123,18,8,2</slash:hit_parade>
|
||||
</item>
|
||||
<textinput rdf:about="https://slashdot.org/search.pl">
|
||||
<title>Search Slashdot</title>
|
||||
<description>Search Slashdot stories</description>
|
||||
<name>query</name>
|
||||
<link>https://slashdot.org/search.pl</link>
|
||||
</textinput>
|
||||
</rdf:RDF>
|
||||
18
src/server/config.go
Normal file
18
src/server/config.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
func Inject(ctx context.Context, port int) context.Context {
|
||||
return context.WithValue(ctx, "server.port", port)
|
||||
}
|
||||
|
||||
func Extract(ctx context.Context) int {
|
||||
v := ctx.Value("server.port")
|
||||
port, ok := v.(int)
|
||||
if !ok {
|
||||
return 10_000
|
||||
}
|
||||
return port
|
||||
}
|
||||
45
src/slow/reader.go
Normal file
45
src/slow/reader.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package slow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type Reader struct {
|
||||
ctx context.Context
|
||||
limiter rate.Limiter
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
var _ io.Reader = &Reader{}
|
||||
|
||||
func NewReader(ctx context.Context, bps rate.Limit, r io.Reader) *Reader {
|
||||
return &Reader{
|
||||
ctx: ctx,
|
||||
limiter: *rate.NewLimiter(bps, int(bps)),
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) Read(b []byte) (int, error) {
|
||||
n, err := r.r.Read(b)
|
||||
|
||||
m := 0
|
||||
burst := r.limiter.Burst()
|
||||
for m < n {
|
||||
page := burst
|
||||
if left := n - m; page > left {
|
||||
page = left
|
||||
}
|
||||
|
||||
if err := r.limiter.WaitN(r.ctx, page); err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
m += page
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
20
src/slow/reader_test.go
Normal file
20
src/slow/reader_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package slow_test
|
||||
|
||||
import "show-rss/src/slow"
|
||||
import "testing"
|
||||
import "context"
|
||||
import "bytes"
|
||||
import "io"
|
||||
|
||||
func TestReader(t *testing.T) {
|
||||
junk := bytes.NewReader(bytes.Repeat([]byte("1"), 256_000))
|
||||
|
||||
slowReader := slow.NewReader(context.Background(), 300_000, junk)
|
||||
|
||||
buff := bytes.NewBuffer(nil)
|
||||
if n, err := io.Copy(buff, slowReader); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n != 256_000 {
|
||||
t.Fatal(n)
|
||||
}
|
||||
}
|
||||
55
src/webhooks/db.go
Normal file
55
src/webhooks/db.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"show-rss/src/db"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func Did(ctx context.Context, method, url, body string) (bool, error) {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
type Did struct {
|
||||
Did int
|
||||
}
|
||||
result, err := db.QueryOne[Did](ctx, `
|
||||
SELECT 1 AS "Did" FROM "webhook.executions" WHERE method=$1 AND url=$2 AND body=$3
|
||||
`, method, url, body)
|
||||
return result.Did > 0 && err == nil, err
|
||||
}
|
||||
|
||||
func Record(ctx context.Context, method, url, body string) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
id := uuid.New().String()
|
||||
return db.Exec(ctx, `
|
||||
INSERT INTO "webhook.executions" (
|
||||
id,
|
||||
executed_at,
|
||||
method,
|
||||
url,
|
||||
body
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
`,
|
||||
id, now, method, url, body,
|
||||
)
|
||||
}
|
||||
|
||||
func initDB(ctx context.Context) error {
|
||||
return db.InitializeSchema(ctx, "webhooks", []string{
|
||||
`CREATE TABLE "webhook.executions" (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
executed_at TIMESTAMP NOT NULL
|
||||
)`,
|
||||
`ALTER TABLE "webhook.executions" ADD COLUMN method TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE "webhook.executions" ADD COLUMN url TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE "webhook.executions" ADD COLUMN body TEXT NOT NULL DEFAULT ''`,
|
||||
})
|
||||
}
|
||||
34
src/webhooks/db_test.go
Normal file
34
src/webhooks/db_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package webhooks_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"show-rss/src/db"
|
||||
"show-rss/src/webhooks"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWebhooks(t *testing.T) {
|
||||
ctx := db.Test(t, context.Background())
|
||||
|
||||
if did, err := webhooks.Did(ctx, "m", "u", "b"); err != nil {
|
||||
t.Errorf("cannot Did() empty: %v", err)
|
||||
} else if did {
|
||||
t.Errorf("wrong Did() empty: %v", did)
|
||||
}
|
||||
|
||||
if err := webhooks.Record(ctx, "m", "u", "b"); err != nil {
|
||||
t.Errorf("cannot Record() empty: %v", err)
|
||||
}
|
||||
|
||||
if did, err := webhooks.Did(ctx, "m", "u", "b"); err != nil {
|
||||
t.Errorf("cannot Did() one: %v", err)
|
||||
} else if !did {
|
||||
t.Errorf("wrong Did() one: %v", did)
|
||||
}
|
||||
|
||||
if did, err := webhooks.Did(ctx, "m2", "u", "b"); err != nil {
|
||||
t.Errorf("cannot Did() wrong one: %v", err)
|
||||
} else if did {
|
||||
t.Errorf("wrong Did() wrong one: %v", did)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user