1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
|
.Dd October 1, 2026
.Dt CALWEB 1
.Os
.Sh NAME
.Nm calweb
.Nd the primary Google calendar of one account, as text, with confirmed writes
.Sh SYNOPSIS
.Nm
.Fl -client-id-cmd Ar cmd
.Fl -client-secret-cmd Ar cmd
.Op Fl -listen Ar addr
.Op Fl -db Ar path
.Op Fl -timezone Ar zone
.Sh DESCRIPTION
.Nm
answers questions about one Google account's primary calendar over HTTP, in the
text rendering the mail frontends of this tree use
.Pq Li text/llm ,
and lets a client
.Em propose
changes that a person then confirms in a browser.
.Pp
Every read asks Google; nothing about the calendar is stored.
The only state is the OAuth grant and the proposals, in one SQLite file.
.Pp
Nothing
.Nm
sends to Google can invite or notify anybody: it has no attendees field, every
write carries
.Li sendUpdates=none ,
and it refuses to propose changes to events somebody else organised or that
have attendees.
See
.Xr calweb 7
for why each of these holds.
.Sh OPTIONS
.Bl -tag -width Ds
.It Fl -client-id-cmd Ar cmd , Fl -client-secret-cmd Ar cmd
Shell commands whose first line of output is the OAuth client id and secret of
a
.Dq Desktop app
client, typically
.Xr pass 1
invocations.
Run once at startup.
.It Fl -listen Ar addr
Default
.Li 127.0.0.1:8779 .
Must be a loopback address; anything else is refused at startup, because
.Nm
has no authentication.
The OAuth redirect is built from this address, so it must be the one the
browser reaches.
.It Fl -db Ar path
Default
.Pa ~/.local/state/calweb/calweb.db .
Created mode 0600.
.It Fl -timezone Ar zone
Default
.Li Europe/Berlin .
Times are shown in it and form fields are read in it.
.El
.Sh LOGGING IN
A new database has no grant, and every route then answers 503 saying so.
A person opens
.Pa http://127.0.0.1:8779/login
in a browser, which redirects to Google; after consent Google redirects back to
.Pa /oauth/callback
and the refresh token is stored.
This is never done by a client on its own.
.Pp
If Google later answers
.Li invalid_grant
\(en access revoked, password changed, or the client's consent screen left in
.Dq Testing ,
which expires grants after seven days \(en the grant is deleted and the same
503 appears, so logging in again is the same step.
.Sh ROUTES
Every route answers text, as
.Li text/llm
when asked for with
.Li Accept: text/llm
or
.Li ?view=llm ,
otherwise as
.Li text/plain .
The single exception is the HTML page of a proposal.
.Ss Reading
.Bl -tag -width Ds
.It Pa /
The agenda, grouped by day.
.Ar since
defaults to today and
.Ar until
to fourteen days later.
Both take
.Li YYYY-MM-DD ,
.Li today ,
.Li tomorrow ,
.Li yesterday ,
or an offset in days, weeks or months
.Pq Li 14d , +2w , -1m
that points forward unless it starts with a minus \(en the reverse of
.Xr mailweb 1 ,
where offsets point into the past.
.Ar until
includes its whole day.
A window holding more than 500 events is cut there and says so.
.It Pa /search?q= Ns Ar words
Google's own search over title, description and place.
The window defaults to one year back to one year ahead and takes the same
parameters as the agenda.
.It Pa /event/ Ns Ar id
One event with its description, organiser, attendees, recurrence, and whether
.Nm
will propose changes to it.
The description is framed as text written by somebody else.
.El
.Ss Proposing
All three take
.Li application/x-www-form-urlencoded
and answer 201 with the URL of the proposal.
None of them writes to Google.
.Bl -tag -width Ds
.It Pa /events Pq POST
A new event.
.Ar summary
is required.
Either
.Ar start
and
.Ar end
as
.Li YYYY-MM-DD HH:MM ,
or
.Ar date
for an all-day event, with optional
.Ar until
as its last day, inclusive.
.Ar location
and
.Ar description
are optional.
A field named
.Ar attendees ,
.Ar attendee ,
.Ar guests
or
.Ar invite
refuses the request.
.It Pa /event/ Ns Ar id Ns Pa /edit Pq POST
The same fields, each optional; only those present change, and a present but
empty one is cleared.
.It Pa /event/ Ns Ar id Ns Pa /delete Pq POST
No fields.
.El
.Pp
Edit and delete fetch the event first and record its etag; they answer 403 for
an event organised by somebody else, one with attendees, one occurrence of a
recurring series, or a special event type, saying which.
.Ss Confirming
.Bl -tag -width Ds
.It Pa /proposals
Proposals waiting for confirmation, then the ten most recently closed.
.It Pa /proposal/ Ns Ar token
In a browser: an HTML page showing each field before and after, with a button
that writes the change and one that discards it.
As text: the same content, without buttons.
.It Pa /proposal/ Ns Ar token Ns Pa /apply Pq POST
Writes the change to Google, then redirects back to the proposal, which now
shows what Google stored.
An edit or delete is sent with
.Li If-Match
of the recorded etag; if the event changed in Google since, nothing is written
and the response says so.
Applying an already applied or discarded proposal is refused with 409.
A request whose
.Li Origin
or
.Li Sec-Fetch-Site
says it came from another site is refused with 403.
.It Pa /proposal/ Ns Ar token Ns Pa /discard Pq POST
Closes the proposal without writing anything.
A proposal already applied or discarded is refused with 409: an applied change
is in Google, and undoing it is a new edit proposal, not a discard.
.El
.Pp
Once applied, the text rendering of a proposal has a
.Li result
section naming the event's
.Pa /event/ Ns Ar id ,
which is how to find a newly created event to edit it further.
.Ss Login
.Bl -tag -width Ds
.It Pa /login
Redirects to Google's consent screen.
.It Pa /oauth/callback
Google's redirect target; stores the grant.
.El
.Sh FILES
.Bl -tag -width Ds
.It Pa ~/.local/state/calweb/calweb.db
The grant
.Pq Li oauth ,
logins in progress
.Pq Li oauth_pending ,
and every proposal ever made
.Pq Li proposals ,
applied ones keeping what Google stored.
The refresh token is stored unencrypted; the file is mode 0600.
.It Pa calweb.service
The systemd user unit, beside the sources.
.El
.Sh EXAMPLES
.Bd -literal
$ curl -s -H 'Accept: text/llm' 'localhost:8779/?since=today&until=+7d'
$ curl -s -H 'Accept: text/llm' 'localhost:8779/search?q=Einführung'
$ curl -s -H 'Accept: text/llm' localhost:8779/events \e
--data-urlencode 'summary=Einführung BA VL' \e
--data-urlencode 'start=2026-10-09 10:00' \e
--data-urlencode 'end=2026-10-09 13:00' \e
--data-urlencode 'location=Raum 2107, Gebäude D'
.Ed
.Pp
The last answers with a proposal URL to open in a browser.
.Ss Places
Google Calendar treats
.Ar location
as a text search when it is opened on a map, so a readable address
.Pq Ql Universität Augsburg, Gebäude D, Universitätsstraße 10
is the useful thing to put there; a room number in it only makes the search
worse.
Exact positions go into
.Ar description
as CoMaps links, one per place, with coordinates taken from OpenStreetMap:
.Bd -literal
https://comaps.at/map?v=1&ll=48.335118,10.896656&n=Uni%20Augsburg%20Geb%C3%A4ude%20D
.Ed
.Pp
.Li ll
is latitude,longitude and
.Li n
the URL-encoded label; several
.Li ll Ns / Ns Li n
pairs may follow one another.
The format is documented at
.Lk https://comaps.at/api .
Coordinates of a named building come from the Overpass API, which answers 406
unless the request carries a
.Li User-Agent
and
.Li "Accept: application/json" ;
.Ql out tags center\&;
gives one point per building outline.
.Sh SEE ALSO
.Xr mailweb 1 ,
.Xr calweb 7
.Sh AUTHORS
.An Profpatsch Aq mail@profpatsch.de
.Sh CAVEATS
Only the primary calendar is reachable.
.Pp
A response is only as current as the request: there is no cache, so the same
URL twice may differ.
.Pp
The confirmation step guards against mistakes, not against a client that
ignores it: anything that can reach the listen address can also send the apply
request.
|