Compare commits

...

129 Commits

Author SHA1 Message Date
bel
18ac13fd57 should not execute feed if version.url=="" 2025-12-10 08:40:28 -07:00
bel
375fc1000a feeds for each runs all and returns all errs 2025-12-10 08:38:59 -07:00
bel
47c7aa74d3 uncap retries on main 2025-12-10 08:23:54 -07:00
bel
a6aad2820d cap retries 2025-12-10 08:13:45 -07:00
bel
b26afcb325 log 2025-12-02 16:30:10 -07:00
bel
613bfdf96e transcode entrypoint 2025-12-02 16:28:55 -07:00
Bel LaPointe
81507319dd disable by default 2025-12-02 16:25:15 -07:00
bel
50ad3bb3db install_scratch.sh 2025-11-30 09:00:47 -07:00
bel
07992b6636 delete less accidentally clickable 2025-11-30 08:56:37 -07:00
bel
9583234df5 do not list deleted (url == "") 2025-11-30 08:54:28 -07:00
bel
2943362587 if POST /?delete then DELETE 2025-11-30 08:54:18 -07:00
bel
cbd4e32022 DELETE /v1/feeds/abc updates all fields to "" 2025-11-30 08:44:27 -07:00
bel
727b4fdea6 from testdata to public for .html 2025-11-30 08:40:15 -07:00
bel
fd7dcafd4e timeout ass to srt 2025-06-01 10:28:43 -06:00
Bel LaPointe
4fbc96b96f dont fail on err but log all 2025-06-01 10:27:00 -06:00
Bel LaPointe
0afb6535b6 impl -e=best-ass-to-srt 2025-06-01 09:55:01 -06:00
Bel LaPointe
10a40d4a54 nopanik choose best lang 2025-06-01 09:52:18 -06:00
Bel LaPointe
4bdfbd1f06 panik till fixed 2025-05-31 11:15:00 -06:00
Bel LaPointe
44bcc0ba2e bestasstosrt does not also remove all sub streams from mkv 2025-05-31 11:14:43 -06:00
Bel LaPointe
b17801060e refactor for fix the world 2025-05-31 11:06:59 -06:00
bel
99c1061a18 fix asses.Next and do asses.Record 2025-05-31 10:56:04 -06:00
bel
67840f6b28 asses does not fail on individual failure 2025-05-30 06:25:06 -06:00
bel
cb44644475 -e=deport-ass filename 2025-05-26 21:41:39 -06:00
bel
6626077201 log asses.Main new dir processing 2025-05-25 11:49:16 -06:00
bel
9c0129f968 if ffmpeg -i .name.ass fails, then rm .name.ass 2025-05-25 11:46:36 -06:00
Bel LaPointe
8efffd0fe4 no size="..." in .srt 2025-05-22 11:29:14 -06:00
bel
fe02d1624f fix now() InLocation(local), deadline per-one 2025-05-17 21:24:32 -06:00
bel
75b7e21bec asses.Next returns midnight if outside working hours 2025-05-17 21:17:26 -06:00
bel
be148f5de5 assing 12am-8am only 2025-05-17 21:10:44 -06:00
bel
a74f741298 deterministic rand for rescan trheshold per file 2025-05-17 21:05:19 -06:00
bel
fba3d635ea if optimistically assuming cksum unchanged, then do not get cksum, just update checked, modified 2025-05-17 20:57:41 -06:00
bel
8f18cbae3a fix small Read() big rate for slow.Reader 2025-05-17 20:53:15 -06:00
bel
d73b63f43c read fast 2025-05-17 20:41:23 -06:00
bel
f57560ebfc fix WEEKS to recheck and assume empty modified has happy cksum 2025-05-17 20:36:59 -06:00
bel
fc66d26c10 log if no check 2025-05-17 20:25:54 -06:00
bel
bd5ae006a1 rate limit asses.Cksum to 10MBps 2025-05-17 20:22:41 -06:00
bel
11b215d026 slow.Reader{} 2025-05-17 20:20:03 -06:00
bel
d2f0466aae only cksum if previously cksummed 2025-05-17 20:06:42 -06:00
bel
90887d3f11 asses add modtime column to skip cksumming for dupe work 2025-05-17 20:03:53 -06:00
bel
582e35b237 shorter log lines 2025-05-17 19:47:55 -06:00
bel
8aeab5ada5 more log 2025-05-17 19:44:56 -06:00
bel
7dd5af0681 asses log 2025-05-17 19:44:07 -06:00
bel
2897a55842 ffmpeg -y 2025-05-17 19:37:10 -06:00
Bel LaPointe
8b668af899 accept $NO_DEPORT 2025-05-08 16:58:34 -06:00
Bel LaPointe
1c7dafc78b asses.One(path) seems to deport ass neato 2025-05-08 16:56:58 -06:00
Bel LaPointe
2e8c8d3d39 stubmore 2025-05-08 16:02:02 -06:00
Bel LaPointe
5e6fd81921 io.eof ready 2025-05-08 15:58:05 -06:00
Bel LaPointe
b9036ed950 testdata mkv 2025-05-08 15:54:54 -06:00
Bel LaPointe
bfbc2b6e7f impl asses skips if cksum matches or a lotta time passes 2025-05-08 15:48:08 -06:00
Bel LaPointe
6b51a0c0a3 impl asses.checkLast(), .check() 2025-05-08 15:40:53 -06:00
Bel LaPointe
137fdf07ed stub cmd.asses 2025-05-08 15:30:51 -06:00
Bel LaPointe
64c4d1908a rm unused 2025-05-08 15:04:44 -06:00
Bel LaPointe
aad5959350 rm unused 2025-05-08 15:04:37 -06:00
Bel LaPointe
f7f44d6615 refactor out cronning 2025-05-08 15:04:04 -06:00
Bel LaPointe
14e80ac2c3 stub asses 2025-05-08 14:55:54 -06:00
Bel LaPointe
6259a4f179 from mutex to semaphore chan 2025-05-08 11:35:41 -06:00
Bel LaPointe
3ac7ae63b6 db locks rather than returning dbInUse errs 2025-05-08 11:31:37 -06:00
Bel LaPointe
786ea3ef8f cache css pls 2025-05-08 11:08:12 -06:00
bel
c3bf31894c default nyaa 2025-05-08 11:03:37 -06:00
bel
e8b6396760 autofill empty form 2025-05-08 11:01:24 -06:00
bel
5470576b10 cron logs 2025-05-07 22:32:36 -06:00
bel
8f0c62bd77 runs 2025-05-07 22:27:06 -06:00
bel
a0ddc7f25f embed fs 2025-05-07 22:15:01 -06:00
bel
e6f551913a / == /experimental/ui 2025-05-07 22:05:44 -06:00
Bel LaPointe
c7375949c2 cli can pass -p 10_000 2025-05-07 20:22:59 -06:00
Bel LaPointe
49064f1ea2 cli can pass -db /path/to/sql.db 2025-05-07 19:45:52 -06:00
Bel LaPointe
2f503497e9 update test for new /v1/feeds status code 2025-05-07 19:45:39 -06:00
bel
5acbd00ee0 src/cmd/* tests are failing 2025-05-05 23:07:51 -06:00
bel
b48e596853 /v1/feeds redirects to /experimental/ui 2025-05-05 23:06:40 -06:00
bel
d7f098bea0 ui can edit 2025-05-05 22:51:06 -06:00
bel
bd67eb0dfe impl feeds.Update 2025-05-05 22:41:37 -06:00
bel
7a94c74226 ui title links to home 2025-05-05 22:17:48 -06:00
bel
1eedf8fce8 someday 2025-05-05 22:14:57 -06:00
bel
bf23d6a9cf wish i could preview items but too lazy 2025-05-05 22:10:04 -06:00
bel
0271f84948 ui has multi-page edit vs new even tho they the same page but templates a lil diff 2025-05-05 21:33:31 -06:00
bel
0e3e6c54de fix tests vs running 2025-05-05 21:32:37 -06:00
bel
352eff2691 webhooks can be vpntor:///outdir 2025-05-05 21:26:19 -06:00
bel
dbfd33f55e index.tml to namespan 2025-05-04 11:36:56 -06:00
bel
f2e828a9eb no panic for sigint 2025-05-04 10:57:43 -06:00
bel
31f7facac7 schema out of xaction 2025-05-04 10:56:57 -06:00
bel
0399fc9316 map nyaa://q=X to https://nyaa.si/.../ 2025-05-04 10:34:03 -06:00
bel
32a010e697 cron main test localhost ok 2025-05-04 09:58:32 -06:00
Bel LaPointe
b200deb98f selfhost 2025-04-28 22:39:21 -06:00
Bel LaPointe
a4eedc7a86 it is TECHNICALLY mvp 2025-04-28 22:37:10 -06:00
Bel LaPointe
dceebd5755 casing and dummy endpoints 2025-04-28 22:27:01 -06:00
Bel LaPointe
29cb008f38 POST /v1/feeds plain html 2025-04-28 22:15:58 -06:00
Bel LaPointe
e4e5529887 tests pass for POST /v1/feeds 2025-04-28 22:08:32 -06:00
Bel LaPointe
e2eb0afe06 test /v1/feeds POST 2025-04-28 22:06:01 -06:00
Bel LaPointe
754ceac95c test /v1/feeds POST 2025-04-28 22:05:31 -06:00
Bel LaPointe
4c9e6fbe35 a dummy readonly ui but no way to write and nothing written is boring 2025-04-28 21:59:00 -06:00
Bel LaPointe
ac642d0bdf temp ui handler 2025-04-28 21:45:02 -06:00
Bel LaPointe
a59857b549 refactor handler out 2025-04-28 21:39:09 -06:00
Bel LaPointe
efb75f74cb test /v1/vpntor calls vpntor nicely 2025-04-28 21:36:24 -06:00
Bel LaPointe
6f8e2e5c53 server test 2025-04-28 21:16:02 -06:00
Bel LaPointe
55c540e9c2 cron passes main woo 2025-04-28 21:02:45 -06:00
Bel LaPointe
5ed296a3d2 impl webhooks.Record(), .Did() 2025-04-28 20:39:42 -06:00
Bel LaPointe
83026a67d4 refactor feeds schema to db/schema.go 2025-04-28 20:32:03 -06:00
Bel LaPointe
57e77e5d4e each feed version has webhooks to do whatever with it 2025-04-28 20:22:11 -06:00
Bel LaPointe
0628a678d8 cron/main_test uses proxy to read testdata/*.rss 2025-04-27 13:17:02 -06:00
Bel LaPointe
49c44b9df8 lil further cron one 2025-04-27 12:59:33 -06:00
Bel LaPointe
f95563e849 can fetch zenshuu with tag 2025-04-27 12:56:33 -06:00
Bel LaPointe
ba429f6028 feeds always fetches via wghttp proxy 2025-04-27 12:49:59 -06:00
Bel LaPointe
8b67437fd1 feed.Fetch parses and matches many fields 2025-04-27 12:36:01 -06:00
Bel LaPointe
1fed2d648f stub feeds http test 2025-04-27 12:06:40 -06:00
Bel LaPointe
a372df64a5 shouldexecute checks deleted 2025-04-27 12:04:43 -06:00
Bel LaPointe
ab396d1833 move cron parse into feed.ShouldExecute 2025-04-27 12:04:11 -06:00
Bel LaPointe
a097814a62 feeds.versions have a pattern 2025-04-27 11:58:00 -06:00
Bel LaPointe
18fd8dfac5 change to feed.Foo methods instead of passing in feed to package func 2025-04-27 11:55:00 -06:00
Bel LaPointe
ce02422b1d ooo i should put more than just DB in feeds huh 2025-04-27 11:51:04 -06:00
Bel LaPointe
ec1f0e007a debatably better 2025-04-27 11:49:57 -06:00
Bel LaPointe
19b6d180e7 feeds from empty struct 2025-04-27 11:49:20 -06:00
Bel LaPointe
e54c7a76f9 gonna swap from feeds.Feeds 2025-04-27 11:46:20 -06:00
Bel LaPointe
537eaf9801 cron testone tries nonempty 2025-04-27 11:33:59 -06:00
Bel LaPointe
baa97ab62d cron.One.feeds.FOrEach 2025-04-27 11:28:28 -06:00
Bel LaPointe
f57408d003 feeds.ForEach 2025-04-27 11:27:47 -06:00
Bel LaPointe
05587ac28e need a way to interate 2025-04-27 11:21:33 -06:00
Bel LaPointe
7f4f760407 test multi-executions 2025-04-27 11:19:26 -06:00
Bel LaPointe
7f97eecbca feeds.Executed 2025-04-27 11:18:17 -06:00
Bel LaPointe
88ab880a8c not used err 2025-04-27 11:13:09 -06:00
Bel LaPointe
e6d9e356ca hm sqlite transactions dont play with begin commit 2025-04-27 11:12:46 -06:00
Bel LaPointe
b85df7bd31 db can query 1 level nested dotnotation 2025-04-27 11:04:17 -06:00
Bel LaPointe
a199e34730 ooooo just gotta dot notation now 2025-04-26 12:47:17 -06:00
bel
61349beb85 no more cur versions table 2025-04-25 22:44:01 -06:00
bel
a2d1d17e23 feeds tests pass 2025-04-25 21:41:52 -06:00
Bel LaPointe
d77e35596e WIP 2025-04-25 16:47:44 -06:00
Bel LaPointe
94f865174b progressing cron db init 2025-04-25 09:54:20 -06:00
Bel LaPointe
0c5bb025bb thar we GO 2025-04-25 09:42:24 -06:00
bel
031d5f545d what tables... 2025-04-24 22:31:28 -06:00
bel
738992468a ooooo generic db.Query, db.QueryOne 2025-04-24 22:10:56 -06:00
54 changed files with 4979 additions and 50 deletions

19
go.mod
View File

@@ -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
View File

@@ -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
View 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'"

View File

@@ -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
}

View File

@@ -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
View 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`,
})
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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")
}
}

Binary file not shown.

64
src/asses/transcode.go Normal file
View 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
View 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)
})
}

View 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)
}
}

View File

@@ -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)
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
}

View File

@@ -1,10 +0,0 @@
package cron
import (
"context"
"io"
)
func Main(ctx context.Context) error {
return io.EOF
}

133
src/cmd/fetch/main.go Normal file
View 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
View 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
View 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
View 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>

View File

@@ -4,24 +4,31 @@ 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()
switch flags.Entrypoint {
case Defacto:
foos := map[string]func(context.Context) error{
"server": server.Main,
"cron": cron.Main,
"fetch": fetch.Main,
"asses": asses.Main,
}
p := pool.New(len(foos))
defer p.Wait(ctx)
@@ -33,6 +40,82 @@ func Main(ctx context.Context) error {
}
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
}
}

View 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)
}

View 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)
}
})
}

View 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
}

View 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)
}
}
}

View 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;
}
}

View 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
View 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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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 &amp;mdash; and especially those with Ph.D.s &amp;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."&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676561&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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."&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676353&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;mdash; including one by Jon Katz...&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676403&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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 &amp;mdash; especially 12-year-olds."
Also this week, the verified Instagram accounts for Disney+, Star Wars and LucasFilm &amp;mdash; Lucas' film and television production company &amp;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.'"&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676461&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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 &amp;mdash; badly.
And this led to a very lively back-and-forth discussion.
Slashdot's summary of the highlights:&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676497&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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;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/ &amp;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;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.
&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676441&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676377&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;mdash; which is always spitting out hydrogen &amp;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...)&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676331&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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."&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676287&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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 &amp;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 &amp;mdash; which includes challenges and stunt videos &amp;mdash; received 3.6 billion views on YouTube, Appleby said.
Appleby, 28, said he's since bought a Jeep for his mom.
&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676255&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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 &amp;mdash; such as the camera &amp;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 &amp;mdash; if very simplified &amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675857&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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."&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675837&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675803&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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" &amp;mdash; the idea that AI models might soon become conscious and deserve some kind of moral status &amp;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 &amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675877&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;utm_medium=feed</link>
<description>It's a pickup truck "that can change into whatever you need it to be &amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675777&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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>

View 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)
}

View 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
}

View 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)
}
})
}

View File

@@ -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()
}

View 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
View 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()
}

View File

@@ -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
}

View File

@@ -3,14 +3,122 @@ package db
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
)
func QueryOne(ctx context.Context, q string, args ...any) error {
return with(ctx, func(db *sql.DB) error {
row := db.QueryRowContext(ctx, q, args...)
TODO generic and return value
return row.Err()
func QueryOne[T any](ctx context.Context, q string, args ...any) (T, error) {
results, err := Query[T](ctx, q, args...)
if err != nil || len(results) == 0 {
var a T
return a, err
}
if len(results) > 1 {
var a T
return a, fmt.Errorf("expected exactly 1 result but got %d", len(results))
}
return results[0], nil
}
func Query[T any](ctx context.Context, q string, args ...any) ([]T, error) {
var result []T
var a T
var m map[string]any
b, _ := json.Marshal(a)
if err := json.Unmarshal(b, &m); err != nil {
return nil, fmt.Errorf("%T is not map-like: %w", a, err)
}
scanners := func(columns []string) ([]any, error) {
s := make([]any, len(columns))
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
}
}
return s, nil
}
err := with(ctx, func(db *sql.DB) error {
rows, err := db.QueryContext(ctx, q, args...)
if err != nil {
return err
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return err
}
for rows.Next() {
scanners, err := scanners(columns)
if err != nil {
return err
}
if err := rows.Scan(scanners...); err != nil {
return err
}
m := map[string]any{}
for i, column := range columns {
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
if b, err := json.Marshal(m); err != nil {
return err
} else if err := json.Unmarshal(b, &a); err != nil {
return err
} else {
result = append(result, a)
}
}
return rows.Err()
})
return result, err
}
func Exec(ctx context.Context, q string, args ...any) error {
@@ -21,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 sem.With(ctx, func() error {
return foo(db)
})
}

View File

@@ -28,17 +28,17 @@ func TestDB(t *testing.T) {
t.Fatal(err)
}
var result struct {
K string
type result struct {
K string `json:"k"`
}
if got, err := db.QueryOne[result](ctx, `SELECT k FROM test WHERE k='a'`); err != nil {
t.Errorf("failed query one: %w", err)
t.Errorf("failed query one: %v", err)
} else if got.K != "a" {
t.Errorf("bad query one: %+v", got)
}
if gots, err := db.Query[result](ctx, `SELECT k FROM test`); err != nil {
t.Errorf("failed query: %w", err)
t.Errorf("failed query: %v", err)
} else if len(gots) != 2 {
t.Errorf("expected 2 but got %d gots", len(gots))
} else if gots[0].K != "a" {
@@ -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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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 &amp;mdash; and especially those with Ph.D.s &amp;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."&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676561&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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."&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676353&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;mdash; including one by Jon Katz...&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676403&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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 &amp;mdash; especially 12-year-olds."
Also this week, the verified Instagram accounts for Disney+, Star Wars and LucasFilm &amp;mdash; Lucas' film and television production company &amp;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.'"&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676461&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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 &amp;mdash; badly.
And this led to a very lively back-and-forth discussion.
Slashdot's summary of the highlights:&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676497&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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;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/ &amp;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;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.
&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676441&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676377&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;mdash; which is always spitting out hydrogen &amp;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...)&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676331&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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."&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676287&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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 &amp;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 &amp;mdash; which includes challenges and stunt videos &amp;mdash; received 3.6 billion views on YouTube, Appleby said.
Appleby, 28, said he's since bought a Jeep for his mom.
&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23676255&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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 &amp;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 &amp;mdash; such as the camera &amp;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 &amp;mdash; if very simplified &amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675857&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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."&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675837&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675803&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;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" &amp;mdash; the idea that AI models might soon become conscious and deserve some kind of moral status &amp;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 &amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675877&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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&amp;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&amp;utm_medium=feed</link>
<description>It's a pickup truck "that can change into whatever you need it to be &amp;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.&lt;p&gt;&lt;div class="share_submission" style="position:relative;"&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt;
&lt;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"&gt;&lt;img src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;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;amp;utm_medium=feed"&gt;Read more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=23675777&amp;amp;smallembed=1" style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;</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
View 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
View 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
View 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
View 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
View 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)
}
}