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