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 Wonkles string
108 Guesses template.HTML
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
183func (honk *Honk) IsWonked() bool {
184 return honk.Flags&flagIsWonked != 0
185}
186
187type Donk struct {
188 FileID int64
189 XID string
190 Name string
191 Desc string
192 URL string
193 Media string
194 Local bool
195 External bool
196}
197
198type Place struct {
199 Name string
200 Latitude float64
201 Longitude float64
202 Url string
203}
204
205type Duration int64
206
207func (d Duration) String() string {
208 s := time.Duration(d).String()
209 if strings.HasSuffix(s, "m0s") {
210 s = s[:len(s)-2]
211 }
212 if strings.HasSuffix(s, "h0m") {
213 s = s[:len(s)-2]
214 }
215 return s
216}
217
218func parseDuration(s string) time.Duration {
219 didx := strings.IndexByte(s, 'd')
220 if didx != -1 {
221 days, _ := strconv.ParseInt(s[:didx], 10, 0)
222 dur, _ := time.ParseDuration(s[didx:])
223 return dur + 24*time.Hour*time.Duration(days)
224 }
225 dur, _ := time.ParseDuration(s)
226 return dur
227}
228
229type Time struct {
230 StartTime time.Time
231 EndTime time.Time
232 Duration Duration
233}
234
235type Honker struct {
236 ID int64
237 UserID int64
238 Name string
239 XID string
240 Handle string
241 Flavor string
242 Combos []string
243 Meta HonkerMeta
244}
245
246type HonkerMeta struct {
247 Notes string
248}
249
250type SomeThing struct {
251 What int
252 XID string
253 Owner string
254 Name string
255}
256
257const (
258 SomeNothing int = iota
259 SomeActor
260 SomeCollection
261)
262
263var serverName string
264var serverPrefix string
265var masqName string
266var dataDir = "."
267var viewDir = "."
268var iconName = "icon.png"
269var serverMsg template.HTML
270var aboutMsg template.HTML
271var loginMsg template.HTML
272
273func ElaborateUnitTests() {
274}
275
276func unplugserver(hostname string) {
277 db := opendatabase()
278 xid := fmt.Sprintf("%%https://%s/%%", hostname)
279 db.Exec("delete from honkers where xid like ? and flavor = 'dub'", xid)
280 db.Exec("delete from doovers where rcpt like ?", xid)
281}
282
283func reexecArgs(cmd string) []string {
284 args := []string{"-datadir", dataDir}
285 args = append(args, log.Args()...)
286 args = append(args, cmd)
287 return args
288}
289
290var elog, ilog, dlog *golog.Logger
291
292func main() {
293 flag.StringVar(&dataDir, "datadir", dataDir, "data directory")
294 flag.StringVar(&viewDir, "viewdir", viewDir, "view directory")
295 flag.Parse()
296
297 log.Init(log.Options{Progname: "honk", Facility: syslog.LOG_UUCP})
298 elog = log.E
299 ilog = log.I
300 dlog = log.D
301
302 args := flag.Args()
303 cmd := "run"
304 if len(args) > 0 {
305 cmd = args[0]
306 }
307 switch cmd {
308 case "init":
309 initdb()
310 case "upgrade":
311 upgradedb()
312 case "version":
313 fmt.Println(softwareVersion)
314 os.Exit(0)
315 }
316 db := opendatabase()
317 dbversion := 0
318 getconfig("dbversion", &dbversion)
319 if dbversion != myVersion {
320 elog.Fatal("incorrect database version. run upgrade.")
321 }
322 getconfig("servermsg", &serverMsg)
323 getconfig("aboutmsg", &aboutMsg)
324 getconfig("loginmsg", &loginMsg)
325 getconfig("servername", &serverName)
326 getconfig("masqname", &masqName)
327 if masqName == "" {
328 masqName = serverName
329 }
330 serverPrefix = fmt.Sprintf("https://%s/", serverName)
331 getconfig("usersep", &userSep)
332 getconfig("honksep", &honkSep)
333 getconfig("devel", &develMode)
334 getconfig("fasttimeout", &fastTimeout)
335 getconfig("slowtimeout", &slowTimeout)
336 getconfig("signgets", &signGets)
337 prepareStatements(db)
338 switch cmd {
339 case "admin":
340 adminscreen()
341 case "import":
342 if len(args) != 4 {
343 elog.Fatal("import username mastodon|twitter srcdir")
344 }
345 importMain(args[1], args[2], args[3])
346 case "devel":
347 if len(args) != 2 {
348 elog.Fatal("need an argument: devel (on|off)")
349 }
350 switch args[1] {
351 case "on":
352 setconfig("devel", 1)
353 case "off":
354 setconfig("devel", 0)
355 default:
356 elog.Fatal("argument must be on or off")
357 }
358 case "setconfig":
359 if len(args) != 3 {
360 elog.Fatal("need an argument: setconfig key val")
361 }
362 var val interface{}
363 var err error
364 if val, err = strconv.Atoi(args[2]); err != nil {
365 val = args[2]
366 }
367 setconfig(args[1], val)
368 case "adduser":
369 adduser()
370 case "deluser":
371 if len(args) < 2 {
372 fmt.Printf("usage: honk deluser username\n")
373 return
374 }
375 deluser(args[1])
376 case "chpass":
377 if len(args) < 2 {
378 fmt.Printf("usage: honk chpass username\n")
379 return
380 }
381 chpass(args[1])
382 case "cleanup":
383 arg := "30"
384 if len(args) > 1 {
385 arg = args[1]
386 }
387 cleanupdb(arg)
388 case "unplug":
389 if len(args) < 2 {
390 fmt.Printf("usage: honk unplug servername\n")
391 return
392 }
393 name := args[1]
394 unplugserver(name)
395 case "backup":
396 if len(args) < 2 {
397 fmt.Printf("usage: honk backup dirname\n")
398 return
399 }
400 name := args[1]
401 svalbard(name)
402 case "ping":
403 if len(args) < 3 {
404 fmt.Printf("usage: honk ping (from username) (to username or url)\n")
405 return
406 }
407 name := args[1]
408 targ := args[2]
409 user, err := butwhatabout(name)
410 if err != nil {
411 elog.Printf("unknown user")
412 return
413 }
414 ping(user, targ)
415 case "run":
416 serve()
417 case "backend":
418 backendServer()
419 case "test":
420 ElaborateUnitTests()
421 default:
422 elog.Fatal("unknown command")
423 }
424}