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}
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
147func OntIsPresent(ont, noise string) bool {
148 ont = ont[1:]
149 return strings.Contains(noise, ">#"+ont) || strings.Contains(noise, "#<span>"+ont)
150}
151
152type OldRevision struct {
153 Precis string
154 Noise string
155}
156
157const (
158 flagIsAcked = 1
159 flagIsBonked = 2
160 flagIsSaved = 4
161 flagIsUntagged = 8
162 flagIsReacted = 16
163)
164
165func (honk *Honk) IsAcked() bool {
166 return honk.Flags&flagIsAcked != 0
167}
168
169func (honk *Honk) IsBonked() bool {
170 return honk.Flags&flagIsBonked != 0
171}
172
173func (honk *Honk) IsSaved() bool {
174 return honk.Flags&flagIsSaved != 0
175}
176
177func (honk *Honk) IsUntagged() bool {
178 return honk.Flags&flagIsUntagged != 0
179}
180
181func (honk *Honk) IsReacted() bool {
182 return honk.Flags&flagIsReacted != 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 if len(args) < 2 {
376 fmt.Printf("usage: honk chpass username\n")
377 return
378 }
379 chpass(args[1])
380 case "follow":
381 if len(args) < 3 {
382 fmt.Printf("usage: honk follow username url\n")
383 return
384 }
385 user, err := butwhatabout(args[1])
386 if err != nil {
387 fmt.Printf("user not found\n")
388 return
389 }
390 var meta HonkerMeta
391 mj, _ := jsonify(&meta)
392 honkerid, err := savehonker(user, args[2], "", "presub", "", mj)
393 if err != nil {
394 fmt.Printf("had some trouble with that: %s\n", err)
395 return
396 }
397 followyou(user, honkerid, true)
398 case "unfollow":
399 if len(args) < 3 {
400 fmt.Printf("usage: honk unfollow username url\n")
401 return
402 }
403 user, err := butwhatabout(args[1])
404 if err != nil {
405 fmt.Printf("user not found\n")
406 return
407 }
408 row := db.QueryRow("select honkerid from honkers where xid = ? and userid = ? and flavor in ('sub')", args[2], user.ID)
409 var honkerid int64
410 err = row.Scan(&honkerid)
411 if err != nil {
412 fmt.Printf("sorry couldn't find them\n")
413 return
414 }
415 unfollowyou(user, honkerid, true)
416 case "cleanup":
417 arg := "30"
418 if len(args) > 1 {
419 arg = args[1]
420 }
421 cleanupdb(arg)
422 case "unplug":
423 if len(args) < 2 {
424 fmt.Printf("usage: honk unplug servername\n")
425 return
426 }
427 name := args[1]
428 unplugserver(name)
429 case "backup":
430 if len(args) < 2 {
431 fmt.Printf("usage: honk backup dirname\n")
432 return
433 }
434 name := args[1]
435 svalbard(name)
436 case "ping":
437 if len(args) < 3 {
438 fmt.Printf("usage: honk ping (from username) (to username or url)\n")
439 return
440 }
441 name := args[1]
442 targ := args[2]
443 user, err := butwhatabout(name)
444 if err != nil {
445 elog.Printf("unknown user")
446 return
447 }
448 ping(user, targ)
449 case "run":
450 serve()
451 case "backend":
452 backendServer()
453 case "test":
454 ElaborateUnitTests()
455 default:
456 elog.Fatal("unknown command")
457 }
458}