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