honk.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "flag"
20 "fmt"
21 "html/template"
22 golog "log"
23 "log/syslog"
24 notrand "math/rand"
25 "os"
26 "strconv"
27 "strings"
28 "time"
29
30 "humungus.tedunangst.com/r/webs/httpsig"
31 "humungus.tedunangst.com/r/webs/log"
32)
33
34var softwareVersion = "develop"
35
36func init() {
37 notrand.Seed(time.Now().Unix())
38}
39
40type WhatAbout struct {
41 ID int64
42 Name string
43 Display string
44 About string
45 HTAbout template.HTML
46 Onts []string
47 Key string
48 URL string
49 Options UserOptions
50 SecKey httpsig.PrivateKey
51}
52
53type UserOptions struct {
54 SkinnyCSS bool `json:",omitempty"`
55 OmitImages bool `json:",omitempty"`
56 MentionAll bool `json:",omitempty"`
57 InlineQuotes bool `json:",omitempty"`
58 Avatar string `json:",omitempty"`
59 Banner string `json:",omitempty"`
60 MapLink string `json:",omitempty"`
61 Reaction string `json:",omitempty"`
62 MeCount int64
63 ChatCount int64
64}
65
66type KeyInfo struct {
67 keyname string
68 seckey httpsig.PrivateKey
69}
70
71const serverUID int64 = -2
72const readyLuserOne int64 = 1
73
74type Honk struct {
75 ID int64
76 UserID int64
77 Username string
78 What string
79 Honker string
80 Handle string
81 Handles string
82 Oonker string
83 Oondle string
84 XID string
85 RID string
86 Date time.Time
87 URL string
88 Noise string
89 Precis string
90 Format string
91 Convoy string
92 Audience []string
93 Public bool
94 Whofore int64
95 Replies []*Honk
96 Flags int64
97 HTPrecis template.HTML
98 HTML template.HTML
99 Style string
100 Open string
101 Donks []*Donk
102 Onts []string
103 Place *Place
104 Time *Time
105 Mentions []Mention
106 Badonks []Badonk
107}
108
109type Badonk struct {
110 Who string
111 What string
112}
113
114type Chonk struct {
115 ID int64
116 UserID int64
117 XID string
118 Who string
119 Target string
120 Date time.Time
121 Noise string
122 Format string
123 Donks []*Donk
124 Handle string
125 HTML template.HTML
126}
127
128type Chatter struct {
129 Target string
130 Chonks []*Chonk
131}
132
133type Mention struct {
134 Who string
135 Where string
136}
137
138func (mention *Mention) IsPresent(noise string) bool {
139 nick := strings.TrimLeft(mention.Who, "@")
140 idx := strings.IndexByte(nick, '@')
141 if idx != -1 {
142 nick = nick[:idx]
143 }
144 return strings.Contains(noise, ">@"+nick) || strings.Contains(noise, "@<span>"+nick)
145}
146
147func OntIsPresent(ont, noise string) bool {
148 ont = strings.ToLower(ont[1:])
149 idx := strings.IndexByte(noise, '#')
150 for idx >= 0 {
151 if strings.HasPrefix(noise[idx:], "#<span>") {
152 idx += 6
153 }
154 idx += 1
155 if idx+len(ont) >= len(noise) {
156 return false
157 }
158 test := noise[idx : idx+len(ont)]
159 test = strings.ToLower(test)
160 if test == ont {
161 return true
162 }
163 newidx := strings.IndexByte(noise[idx:], '#')
164 if newidx == -1 {
165 return false
166 }
167 idx += newidx
168 }
169 return false
170}
171
172type OldRevision struct {
173 Precis string
174 Noise string
175}
176
177const (
178 flagIsAcked = 1
179 flagIsBonked = 2
180 flagIsSaved = 4
181 flagIsUntagged = 8
182 flagIsReacted = 16
183)
184
185func (honk *Honk) IsAcked() bool {
186 return honk.Flags&flagIsAcked != 0
187}
188
189func (honk *Honk) IsBonked() bool {
190 return honk.Flags&flagIsBonked != 0
191}
192
193func (honk *Honk) IsSaved() bool {
194 return honk.Flags&flagIsSaved != 0
195}
196
197func (honk *Honk) IsUntagged() bool {
198 return honk.Flags&flagIsUntagged != 0
199}
200
201func (honk *Honk) IsReacted() bool {
202 return honk.Flags&flagIsReacted != 0
203}
204
205type Donk struct {
206 FileID int64
207 XID string
208 Name string
209 Desc string
210 URL string
211 Media string
212 Local bool
213 External bool
214}
215
216type Place struct {
217 Name string
218 Latitude float64
219 Longitude float64
220 Url string
221}
222
223type Duration int64
224
225func (d Duration) String() string {
226 s := time.Duration(d).String()
227 if strings.HasSuffix(s, "m0s") {
228 s = s[:len(s)-2]
229 }
230 if strings.HasSuffix(s, "h0m") {
231 s = s[:len(s)-2]
232 }
233 return s
234}
235
236func parseDuration(s string) time.Duration {
237 didx := strings.IndexByte(s, 'd')
238 if didx != -1 {
239 days, _ := strconv.ParseInt(s[:didx], 10, 0)
240 dur, _ := time.ParseDuration(s[didx:])
241 return dur + 24*time.Hour*time.Duration(days)
242 }
243 dur, _ := time.ParseDuration(s)
244 return dur
245}
246
247type Time struct {
248 StartTime time.Time
249 EndTime time.Time
250 Duration Duration
251}
252
253type Honker struct {
254 ID int64
255 UserID int64
256 Name string
257 XID string
258 Handle string
259 Flavor string
260 Combos []string
261 Meta HonkerMeta
262}
263
264type HonkerMeta struct {
265 Notes string
266}
267
268type SomeThing struct {
269 What int
270 XID string
271 Owner string
272 Name string
273}
274
275const (
276 SomeNothing int = iota
277 SomeActor
278 SomeCollection
279)
280
281var serverName string
282var serverPrefix string
283var masqName string
284var dataDir = "."
285var viewDir = "."
286var iconName = "icon.png"
287var serverMsg template.HTML
288var aboutMsg template.HTML
289var loginMsg template.HTML
290
291func ElaborateUnitTests() {
292}
293
294func unplugserver(hostname string) {
295 db := opendatabase()
296 xid := fmt.Sprintf("%%https://%s/%%", hostname)
297 db.Exec("delete from honkers where xid like ? and flavor = 'dub'", xid)
298 db.Exec("delete from doovers where rcpt like ?", xid)
299}
300
301func reexecArgs(cmd string) []string {
302 args := []string{"-datadir", dataDir}
303 args = append(args, log.Args()...)
304 args = append(args, cmd)
305 return args
306}
307
308var elog, ilog, dlog *golog.Logger
309
310func main() {
311 flag.StringVar(&dataDir, "datadir", dataDir, "data directory")
312 flag.StringVar(&viewDir, "viewdir", viewDir, "view directory")
313 flag.Parse()
314
315 log.Init(log.Options{Progname: "honk", Facility: syslog.LOG_UUCP})
316 elog = log.E
317 ilog = log.I
318 dlog = log.D
319
320 args := flag.Args()
321 cmd := "run"
322 if len(args) > 0 {
323 cmd = args[0]
324 }
325 switch cmd {
326 case "init":
327 initdb()
328 case "upgrade":
329 upgradedb()
330 case "version":
331 fmt.Println(softwareVersion)
332 os.Exit(0)
333 }
334 db := opendatabase()
335 dbversion := 0
336 getconfig("dbversion", &dbversion)
337 if dbversion != myVersion {
338 elog.Fatal("incorrect database version. run upgrade.")
339 }
340 getconfig("servermsg", &serverMsg)
341 getconfig("aboutmsg", &aboutMsg)
342 getconfig("loginmsg", &loginMsg)
343 getconfig("servername", &serverName)
344 getconfig("masqname", &masqName)
345 if masqName == "" {
346 masqName = serverName
347 }
348 serverPrefix = fmt.Sprintf("https://%s/", serverName)
349 getconfig("usersep", &userSep)
350 getconfig("honksep", &honkSep)
351 getconfig("devel", &develMode)
352 getconfig("fasttimeout", &fastTimeout)
353 getconfig("slowtimeout", &slowTimeout)
354 getconfig("signgets", &signGets)
355 prepareStatements(db)
356 switch cmd {
357 case "admin":
358 adminscreen()
359 case "import":
360 if len(args) != 4 {
361 elog.Fatal("import username mastodon|twitter srcdir")
362 }
363 importMain(args[1], args[2], args[3])
364 case "devel":
365 if len(args) != 2 {
366 elog.Fatal("need an argument: devel (on|off)")
367 }
368 switch args[1] {
369 case "on":
370 setconfig("devel", 1)
371 case "off":
372 setconfig("devel", 0)
373 default:
374 elog.Fatal("argument must be on or off")
375 }
376 case "setconfig":
377 if len(args) != 3 {
378 elog.Fatal("need an argument: setconfig key val")
379 }
380 var val interface{}
381 var err error
382 if val, err = strconv.Atoi(args[2]); err != nil {
383 val = args[2]
384 }
385 setconfig(args[1], val)
386 case "adduser":
387 adduser()
388 case "deluser":
389 if len(args) < 2 {
390 fmt.Printf("usage: honk deluser username\n")
391 return
392 }
393 deluser(args[1])
394 case "chpass":
395 if len(args) < 2 {
396 fmt.Printf("usage: honk chpass username\n")
397 return
398 }
399 chpass(args[1])
400 case "follow":
401 if len(args) < 3 {
402 fmt.Printf("usage: honk follow username url\n")
403 return
404 }
405 user, err := butwhatabout(args[1])
406 if err != nil {
407 fmt.Printf("user not found\n")
408 return
409 }
410 var meta HonkerMeta
411 mj, _ := jsonify(&meta)
412 honkerid, err := savehonker(user, args[2], "", "presub", "", mj)
413 if err != nil {
414 fmt.Printf("had some trouble with that: %s\n", err)
415 return
416 }
417 followyou(user, honkerid, true)
418 case "unfollow":
419 if len(args) < 3 {
420 fmt.Printf("usage: honk unfollow username url\n")
421 return
422 }
423 user, err := butwhatabout(args[1])
424 if err != nil {
425 fmt.Printf("user not found\n")
426 return
427 }
428 row := db.QueryRow("select honkerid from honkers where xid = ? and userid = ? and flavor in ('sub')", args[2], user.ID)
429 var honkerid int64
430 err = row.Scan(&honkerid)
431 if err != nil {
432 fmt.Printf("sorry couldn't find them\n")
433 return
434 }
435 unfollowyou(user, honkerid, true)
436 case "sendmsg":
437 if len(args) < 4 {
438 fmt.Printf("usage: honk send username filename rcpt\n")
439 return
440 }
441 user, err := butwhatabout(args[1])
442 if err != nil {
443 fmt.Printf("user not found\n")
444 return
445 }
446 data, err := os.ReadFile(args[2])
447 if err != nil {
448 fmt.Printf("can't read file\n")
449 return
450 }
451 deliverate(user.ID, args[3], data)
452 case "cleanup":
453 arg := "30"
454 if len(args) > 1 {
455 arg = args[1]
456 }
457 cleanupdb(arg)
458 case "unplug":
459 if len(args) < 2 {
460 fmt.Printf("usage: honk unplug servername\n")
461 return
462 }
463 name := args[1]
464 unplugserver(name)
465 case "backup":
466 if len(args) < 2 {
467 fmt.Printf("usage: honk backup dirname\n")
468 return
469 }
470 name := args[1]
471 svalbard(name)
472 case "ping":
473 if len(args) < 3 {
474 fmt.Printf("usage: honk ping (from username) (to username or url)\n")
475 return
476 }
477 name := args[1]
478 targ := args[2]
479 user, err := butwhatabout(name)
480 if err != nil {
481 elog.Printf("unknown user")
482 return
483 }
484 ping(user, targ)
485 case "run":
486 serve()
487 case "backend":
488 backendServer()
489 case "test":
490 ElaborateUnitTests()
491 default:
492 elog.Fatal("unknown command")
493 }
494}