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 MapLink string `json:",omitempty"`
60 Reaction string `json:",omitempty"`
61 MeCount int64
62 ChatCount int64
63}
64
65type KeyInfo struct {
66 keyname string
67 seckey httpsig.PrivateKey
68}
69
70const serverUID int64 = -2
71
72type Honk struct {
73 ID int64
74 UserID int64
75 Username string
76 What string
77 Honker string
78 Handle string
79 Handles string
80 Oonker string
81 Oondle string
82 XID string
83 RID string
84 Date time.Time
85 URL string
86 Noise string
87 Precis string
88 Format string
89 Convoy string
90 Audience []string
91 Public bool
92 Whofore int64
93 Replies []*Honk
94 Flags int64
95 HTPrecis template.HTML
96 HTML template.HTML
97 Style string
98 Open string
99 Donks []*Donk
100 Onts []string
101 Place *Place
102 Time *Time
103 Mentions []Mention
104 Badonks []Badonk
105 Wonkles string
106 Guesses template.HTML
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 nick += "<"
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}