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 += 5
153 } else {
154 idx += 1
155 }
156 if idx + len(ont) + 1 > len(noise) {
157 return false
158 }
159 test := noise[idx:idx+len(ont)]
160 test = strings.ToLower(test)
161 if test == ont {
162 return true
163 }
164 newidx := strings.IndexByte(noise[idx:], '#')
165 if newidx == -1 {
166 return false
167 }
168 idx += newidx
169 }
170 return false
171}
172
173type OldRevision struct {
174 Precis string
175 Noise string
176}
177
178const (
179 flagIsAcked = 1
180 flagIsBonked = 2
181 flagIsSaved = 4
182 flagIsUntagged = 8
183 flagIsReacted = 16
184)
185
186func (honk *Honk) IsAcked() bool {
187 return honk.Flags&flagIsAcked != 0
188}
189
190func (honk *Honk) IsBonked() bool {
191 return honk.Flags&flagIsBonked != 0
192}
193
194func (honk *Honk) IsSaved() bool {
195 return honk.Flags&flagIsSaved != 0
196}
197
198func (honk *Honk) IsUntagged() bool {
199 return honk.Flags&flagIsUntagged != 0
200}
201
202func (honk *Honk) IsReacted() bool {
203 return honk.Flags&flagIsReacted != 0
204}
205
206type Donk struct {
207 FileID int64
208 XID string
209 Name string
210 Desc string
211 URL string
212 Media string
213 Local bool
214 External bool
215}
216
217type Place struct {
218 Name string
219 Latitude float64
220 Longitude float64
221 Url string
222}
223
224type Duration int64
225
226func (d Duration) String() string {
227 s := time.Duration(d).String()
228 if strings.HasSuffix(s, "m0s") {
229 s = s[:len(s)-2]
230 }
231 if strings.HasSuffix(s, "h0m") {
232 s = s[:len(s)-2]
233 }
234 return s
235}
236
237func parseDuration(s string) time.Duration {
238 didx := strings.IndexByte(s, 'd')
239 if didx != -1 {
240 days, _ := strconv.ParseInt(s[:didx], 10, 0)
241 dur, _ := time.ParseDuration(s[didx:])
242 return dur + 24*time.Hour*time.Duration(days)
243 }
244 dur, _ := time.ParseDuration(s)
245 return dur
246}
247
248type Time struct {
249 StartTime time.Time
250 EndTime time.Time
251 Duration Duration
252}
253
254type Honker struct {
255 ID int64
256 UserID int64
257 Name string
258 XID string
259 Handle string
260 Flavor string
261 Combos []string
262 Meta HonkerMeta
263}
264
265type HonkerMeta struct {
266 Notes string
267}
268
269type SomeThing struct {
270 What int
271 XID string
272 Owner string
273 Name string
274}
275
276const (
277 SomeNothing int = iota
278 SomeActor
279 SomeCollection
280)
281
282var serverName string
283var serverPrefix string
284var masqName string
285var dataDir = "."
286var viewDir = "."
287var iconName = "icon.png"
288var serverMsg template.HTML
289var aboutMsg template.HTML
290var loginMsg template.HTML
291
292func ElaborateUnitTests() {
293}
294
295func unplugserver(hostname string) {
296 db := opendatabase()
297 xid := fmt.Sprintf("%%https://%s/%%", hostname)
298 db.Exec("delete from honkers where xid like ? and flavor = 'dub'", xid)
299 db.Exec("delete from doovers where rcpt like ?", xid)
300}
301
302func reexecArgs(cmd string) []string {
303 args := []string{"-datadir", dataDir}
304 args = append(args, log.Args()...)
305 args = append(args, cmd)
306 return args
307}
308
309var elog, ilog, dlog *golog.Logger
310
311func main() {
312 flag.StringVar(&dataDir, "datadir", dataDir, "data directory")
313 flag.StringVar(&viewDir, "viewdir", viewDir, "view directory")
314 flag.Parse()
315
316 log.Init(log.Options{Progname: "honk", Facility: syslog.LOG_UUCP})
317 elog = log.E
318 ilog = log.I
319 dlog = log.D
320
321 args := flag.Args()
322 cmd := "run"
323 if len(args) > 0 {
324 cmd = args[0]
325 }
326 switch cmd {
327 case "init":
328 initdb()
329 case "upgrade":
330 upgradedb()
331 case "version":
332 fmt.Println(softwareVersion)
333 os.Exit(0)
334 }
335 db := opendatabase()
336 dbversion := 0
337 getconfig("dbversion", &dbversion)
338 if dbversion != myVersion {
339 elog.Fatal("incorrect database version. run upgrade.")
340 }
341 getconfig("servermsg", &serverMsg)
342 getconfig("aboutmsg", &aboutMsg)
343 getconfig("loginmsg", &loginMsg)
344 getconfig("servername", &serverName)
345 getconfig("masqname", &masqName)
346 if masqName == "" {
347 masqName = serverName
348 }
349 serverPrefix = fmt.Sprintf("https://%s/", serverName)
350 getconfig("usersep", &userSep)
351 getconfig("honksep", &honkSep)
352 getconfig("devel", &develMode)
353 getconfig("fasttimeout", &fastTimeout)
354 getconfig("slowtimeout", &slowTimeout)
355 getconfig("signgets", &signGets)
356 prepareStatements(db)
357 switch cmd {
358 case "admin":
359 adminscreen()
360 case "import":
361 if len(args) != 4 {
362 elog.Fatal("import username mastodon|twitter srcdir")
363 }
364 importMain(args[1], args[2], args[3])
365 case "devel":
366 if len(args) != 2 {
367 elog.Fatal("need an argument: devel (on|off)")
368 }
369 switch args[1] {
370 case "on":
371 setconfig("devel", 1)
372 case "off":
373 setconfig("devel", 0)
374 default:
375 elog.Fatal("argument must be on or off")
376 }
377 case "setconfig":
378 if len(args) != 3 {
379 elog.Fatal("need an argument: setconfig key val")
380 }
381 var val interface{}
382 var err error
383 if val, err = strconv.Atoi(args[2]); err != nil {
384 val = args[2]
385 }
386 setconfig(args[1], val)
387 case "adduser":
388 adduser()
389 case "deluser":
390 if len(args) < 2 {
391 fmt.Printf("usage: honk deluser username\n")
392 return
393 }
394 deluser(args[1])
395 case "chpass":
396 if len(args) < 2 {
397 fmt.Printf("usage: honk chpass username\n")
398 return
399 }
400 chpass(args[1])
401 case "follow":
402 if len(args) < 3 {
403 fmt.Printf("usage: honk follow username url\n")
404 return
405 }
406 user, err := butwhatabout(args[1])
407 if err != nil {
408 fmt.Printf("user not found\n")
409 return
410 }
411 var meta HonkerMeta
412 mj, _ := jsonify(&meta)
413 honkerid, err := savehonker(user, args[2], "", "presub", "", mj)
414 if err != nil {
415 fmt.Printf("had some trouble with that: %s\n", err)
416 return
417 }
418 followyou(user, honkerid, true)
419 case "unfollow":
420 if len(args) < 3 {
421 fmt.Printf("usage: honk unfollow username url\n")
422 return
423 }
424 user, err := butwhatabout(args[1])
425 if err != nil {
426 fmt.Printf("user not found\n")
427 return
428 }
429 row := db.QueryRow("select honkerid from honkers where xid = ? and userid = ? and flavor in ('sub')", args[2], user.ID)
430 var honkerid int64
431 err = row.Scan(&honkerid)
432 if err != nil {
433 fmt.Printf("sorry couldn't find them\n")
434 return
435 }
436 unfollowyou(user, honkerid, true)
437 case "cleanup":
438 arg := "30"
439 if len(args) > 1 {
440 arg = args[1]
441 }
442 cleanupdb(arg)
443 case "unplug":
444 if len(args) < 2 {
445 fmt.Printf("usage: honk unplug servername\n")
446 return
447 }
448 name := args[1]
449 unplugserver(name)
450 case "backup":
451 if len(args) < 2 {
452 fmt.Printf("usage: honk backup dirname\n")
453 return
454 }
455 name := args[1]
456 svalbard(name)
457 case "ping":
458 if len(args) < 3 {
459 fmt.Printf("usage: honk ping (from username) (to username or url)\n")
460 return
461 }
462 name := args[1]
463 targ := args[2]
464 user, err := butwhatabout(name)
465 if err != nil {
466 elog.Printf("unknown user")
467 return
468 }
469 ping(user, targ)
470 case "run":
471 serve()
472 case "backend":
473 backendServer()
474 case "test":
475 ElaborateUnitTests()
476 default:
477 elog.Fatal("unknown command")
478 }
479}