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