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 Avahex bool `json:",omitempty"`
59 Avatar string `json:",omitempty"`
60 CustomDisplay string `json:",omitempty"`
61 Banner string `json:",omitempty"`
62 MapLink string `json:",omitempty"`
63 Reaction string `json:",omitempty"`
64 MeCount int64
65 ChatCount int64
66}
67
68type KeyInfo struct {
69 keyname string
70 seckey httpsig.PrivateKey
71}
72
73const serverUID int64 = -2
74const readyLuserOne int64 = 1
75
76type Honk struct {
77 ID int64
78 UserID int64
79 Username string
80 DisplayName string
81 What string
82 Honker string
83 Handle string
84 Handles string
85 Oonker string
86 Oondle string
87 XID string
88 RID string
89 Date time.Time
90 DatePretty string
91 URL string
92 Noise string
93 Precis string
94 Format string
95 Convoy string
96 Audience []string
97 Public bool
98 Whofore int64
99 Replies []*Honk
100 Flags int64
101 HTPrecis template.HTML
102 HTML template.HTML
103 Style string
104 Open string
105 Donks []*Donk
106 Onts []string
107 Place *Place
108 Time *Time
109 Mentions []Mention
110 Badonks []Badonk
111}
112
113type Badonk struct {
114 Who string
115 What string
116}
117
118type Chonk struct {
119 ID int64
120 UserID int64
121 XID string
122 Who string
123 Target string
124 Date time.Time
125 Noise string
126 Format string
127 Donks []*Donk
128 Handle string
129 HTML template.HTML
130}
131
132type Chatter struct {
133 Target string
134 Chonks []*Chonk
135}
136
137type Mention struct {
138 Who string
139 Where string
140}
141
142func (mention *Mention) IsPresent(noise string) bool {
143 nick := strings.TrimLeft(mention.Who, "@")
144 idx := strings.IndexByte(nick, '@')
145 if idx != -1 {
146 nick = nick[:idx]
147 }
148 return strings.Contains(noise, ">@"+nick) || strings.Contains(noise, "@<span>"+nick)
149}
150
151type OldRevision struct {
152 Precis string
153 Noise string
154}
155
156const (
157 flagIsAcked = 1
158 flagIsBonked = 2
159 flagIsSaved = 4
160 flagIsUntagged = 8
161 flagIsReacted = 16
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}