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