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